自动同步: 2026-03-24 02:10
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -62,6 +62,10 @@ class KBSettings(BaseSettings):
|
||||
overwrite_archives: bool = Field(
|
||||
default=False, description="覆盖已存在的归档文件(非交互模式)"
|
||||
)
|
||||
enable_semantic_segmentation: bool = Field(
|
||||
default=True,
|
||||
description="启用 LLM 语义分段(默认启用)"
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -17,7 +17,7 @@ import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Optional
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
|
||||
from kb_workflow.pipeline.config import get_settings
|
||||
from kb_workflow.pipeline.processors.base import BaseProcessor
|
||||
@@ -48,19 +48,70 @@ class SRTProcessor(BaseProcessor):
|
||||
def __init__(
|
||||
self,
|
||||
state_manager: Optional[WorkflowStateManager] = None,
|
||||
enable_correction: bool = True
|
||||
enable_correction: bool = True,
|
||||
enable_semantic_segmentation: Optional[bool] = None,
|
||||
enable_cache: bool = True
|
||||
):
|
||||
"""初始化 SRT 处理器
|
||||
|
||||
Args:
|
||||
state_manager: 状态管理器
|
||||
enable_correction: 是否启用 ASR 纠错(默认 True)
|
||||
enable_semantic_segmentation: 是否启用 LLM 语义分段(None 时从配置读取)
|
||||
enable_cache: 是否启用缓存(默认 True,仅在启用语义分段时有效)
|
||||
"""
|
||||
super().__init__(state_manager)
|
||||
|
||||
# 如果未明确指定,从配置读取
|
||||
if enable_semantic_segmentation is None:
|
||||
settings = get_settings()
|
||||
enable_semantic_segmentation = settings.enable_semantic_segmentation
|
||||
logger.info(
|
||||
f"SRTProcessor using config: "
|
||||
f"semantic_segmentation={'enabled' if enable_semantic_segmentation else 'disabled'}"
|
||||
)
|
||||
elif enable_semantic_segmentation:
|
||||
logger.info("SRTProcessor using explicit parameter: semantic_segmentation=enabled")
|
||||
else:
|
||||
logger.info("SRTProcessor using explicit parameter: semantic_segmentation=disabled")
|
||||
|
||||
self.enable_correction = enable_correction
|
||||
self.segmenter = SemanticSegmenter()
|
||||
self.enable_semantic_segmentation = enable_semantic_segmentation
|
||||
self.segmenter = None
|
||||
self.corrector = None
|
||||
|
||||
# 初始化分段器
|
||||
if enable_semantic_segmentation:
|
||||
try:
|
||||
from kb_workflow.srt.semantic_engine import SemanticSegmentationEngine
|
||||
from kb_workflow.srt.semantic_engine import UnifiedSegmentationStrategy
|
||||
from kb_workflow.deep_analyzer import SimpleAIClient
|
||||
|
||||
# 创建 AI 客户端
|
||||
ai_client = SimpleAIClient()
|
||||
if ai_client.api_key:
|
||||
# 初始化缓存
|
||||
cache = None
|
||||
if enable_cache:
|
||||
from kb_workflow.srt.cache import SemanticCache
|
||||
cache = SemanticCache()
|
||||
|
||||
# 创建统一策略,传入 AI 客户端和缓存
|
||||
strategy = UnifiedSegmentationStrategy(
|
||||
ai_client=ai_client,
|
||||
cache=cache
|
||||
)
|
||||
self.segmenter = SemanticSegmentationEngine(strategy=strategy)
|
||||
logger.info(f"Semantic segmentation enabled (cache={'enabled' if cache else 'disabled'})")
|
||||
else:
|
||||
logger.info("AI API key not configured, using rule-based segmenter")
|
||||
self.segmenter = SemanticSegmenter()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to initialize semantic engine: {e}, using rule-based segmenter")
|
||||
self.segmenter = SemanticSegmenter()
|
||||
else:
|
||||
self.segmenter = SemanticSegmenter()
|
||||
|
||||
# 延迟初始化纠错器
|
||||
if enable_correction:
|
||||
try:
|
||||
@@ -102,27 +153,53 @@ class SRTProcessor(BaseProcessor):
|
||||
segments = parse_srt_file(Path(input_data.srt_file))
|
||||
logger.info(f"Parsed {len(segments)} segments")
|
||||
|
||||
# Step 2: 语义分段
|
||||
# Step 2: 加载元数据(需要在分段之前加载,因为 LLM 分段需要元数据)
|
||||
metadata = self._load_metadata(input_data.metadata_file)
|
||||
|
||||
# Step 3: 语义分段
|
||||
logger.info("Segmenting subtitles into paragraphs")
|
||||
paragraphs = self.segmenter.segment(segments)
|
||||
if self.enable_semantic_segmentation:
|
||||
# 使用 LLM 语义分段
|
||||
from kb_workflow.srt.semantic_engine import SemanticSegmentationEngine
|
||||
if isinstance(self.segmenter, SemanticSegmentationEngine):
|
||||
# 返回 VideoSegment 列表(包含标题)
|
||||
paragraphs = self.segmenter.segment(segments, metadata=metadata)
|
||||
else:
|
||||
# 规则分段返回纯文本列表
|
||||
paragraphs = self.segmenter.segment(segments)
|
||||
else:
|
||||
# 规则分段返回纯文本列表
|
||||
paragraphs = self.segmenter.segment(segments)
|
||||
logger.info(f"Generated {len(paragraphs)} paragraphs")
|
||||
|
||||
# Step 3: ASR 纠错(可选)
|
||||
# Step 4: ASR 纠错(可选)
|
||||
corrected = False
|
||||
if self.corrector and paragraphs:
|
||||
logger.info("Applying ASR correction")
|
||||
corrected_paragraphs = []
|
||||
for para in paragraphs:
|
||||
corrected_para = self.corrector.correct(para)
|
||||
# 处理 VideoSegment 或纯文本
|
||||
if hasattr(para, 'content'):
|
||||
# VideoSegment 对象
|
||||
content = para.content
|
||||
corrected_content = self.corrector.correct(content)
|
||||
# 创建新的 VideoSegment 对象(假设不可变)
|
||||
from kb_workflow.srt.semantic_engine import VideoSegment
|
||||
corrected_para = VideoSegment(
|
||||
title=para.title,
|
||||
content=corrected_content,
|
||||
start_index=para.start_index,
|
||||
end_index=para.end_index
|
||||
)
|
||||
else:
|
||||
# 纯文本
|
||||
corrected_para = self.corrector.correct(para)
|
||||
corrected_paragraphs.append(corrected_para)
|
||||
if corrected_para != para:
|
||||
corrected = True
|
||||
paragraphs = corrected_paragraphs
|
||||
logger.info(f"ASR correction applied: {corrected}")
|
||||
|
||||
# Step 4: 加载元数据
|
||||
metadata = self._load_metadata(input_data.metadata_file)
|
||||
|
||||
# Step 5: 生成 Markdown
|
||||
logger.info(f"Generating Markdown: {input_data.output_file}")
|
||||
tags = self._extract_tags(paragraphs)
|
||||
@@ -133,6 +210,12 @@ class SRTProcessor(BaseProcessor):
|
||||
tags=tags
|
||||
)
|
||||
|
||||
# 转换 VideoSegment 为纯文本(用于 SRTOutput)
|
||||
if paragraphs and hasattr(paragraphs[0], 'content'):
|
||||
paragraphs_text = [para.content if hasattr(para, 'content') else para for para in paragraphs]
|
||||
else:
|
||||
paragraphs_text = paragraphs
|
||||
|
||||
# 记录完成状态
|
||||
if self.state_manager:
|
||||
state = self.state_manager.load()
|
||||
@@ -144,7 +227,7 @@ class SRTProcessor(BaseProcessor):
|
||||
return SRTOutput(
|
||||
success=True,
|
||||
output_file=str(input_data.output_file),
|
||||
paragraphs=paragraphs,
|
||||
paragraphs=paragraphs_text,
|
||||
tags=tags,
|
||||
corrected=corrected
|
||||
)
|
||||
@@ -190,14 +273,14 @@ class SRTProcessor(BaseProcessor):
|
||||
|
||||
return raw_metadata
|
||||
|
||||
def _extract_tags(self, paragraphs: List[str]) -> List[str]:
|
||||
def _extract_tags(self, paragraphs: Union[List[str], List[Any]]) -> List[str]:
|
||||
"""从段落中提取标签
|
||||
|
||||
简单实现:不使用 LLM,只返回默认标签。
|
||||
未来可以增强为 LLM 提取。
|
||||
|
||||
Args:
|
||||
paragraphs: 段落列表
|
||||
paragraphs: 段落列表(可以是纯文本或 VideoSegment 对象)
|
||||
|
||||
Returns:
|
||||
标签列表
|
||||
@@ -210,7 +293,7 @@ class SRTProcessor(BaseProcessor):
|
||||
self,
|
||||
output_file: Path,
|
||||
metadata: Dict[str, Any],
|
||||
paragraphs: List[str],
|
||||
paragraphs: Union[List[str], List[Any]], # List[str] 或 List[VideoSegment]
|
||||
tags: List[str]
|
||||
) -> None:
|
||||
"""生成 Markdown 文件
|
||||
@@ -230,12 +313,29 @@ class SRTProcessor(BaseProcessor):
|
||||
# 构建内容
|
||||
content_parts = [frontmatter, ""]
|
||||
|
||||
# 直接添加内容,不生成摘要部分
|
||||
content_parts.append("## 内容")
|
||||
content_parts.append("")
|
||||
for para in paragraphs:
|
||||
content_parts.append(para)
|
||||
# 检查是否是 VideoSegment 类型(LLM 分段结果)
|
||||
from kb_workflow.srt.semantic_engine import VideoSegment
|
||||
is_semantic_segmentation = (
|
||||
paragraphs and
|
||||
hasattr(paragraphs[0], 'title') and
|
||||
hasattr(paragraphs[0], 'content')
|
||||
)
|
||||
|
||||
if is_semantic_segmentation:
|
||||
# LLM 语义分段:包含标题和内容
|
||||
for seg in paragraphs:
|
||||
if seg.title:
|
||||
content_parts.append(f"## {seg.title}")
|
||||
content_parts.append("")
|
||||
content_parts.append(seg.content)
|
||||
content_parts.append("")
|
||||
else:
|
||||
# 规则分段:纯文本
|
||||
content_parts.append("## 内容")
|
||||
content_parts.append("")
|
||||
for para in paragraphs:
|
||||
content_parts.append(para)
|
||||
content_parts.append("")
|
||||
|
||||
# 写入文件
|
||||
output_file.write_text('\n'.join(content_parts), encoding='utf-8')
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+8
@@ -0,0 +1,8 @@
|
||||
"""语义分段缓存模块"""
|
||||
|
||||
from kb_workflow.srt.cache.semantic_cache import (
|
||||
SemanticCache,
|
||||
compute_content_hash
|
||||
)
|
||||
|
||||
__all__ = ['SemanticCache', 'compute_content_hash']
|
||||
Binary file not shown.
Binary file not shown.
+155
@@ -0,0 +1,155 @@
|
||||
"""语义分段缓存管理器
|
||||
|
||||
使用内容哈希(SHA256)作为缓存键,存储在本地文件系统。
|
||||
|
||||
功能:
|
||||
- 计算内容的 SHA256 哈希作为缓存键
|
||||
- 缓存 SegmentationResult 到本地文件系统
|
||||
- 支持版本控制,便于未来格式变更
|
||||
- 错误容错,缓存失败不影响主流程
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from kb_workflow.srt.semantic_engine import SegmentationResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def compute_content_hash(content: str) -> str:
|
||||
"""计算内容的 SHA256 哈希作为缓存键
|
||||
|
||||
Args:
|
||||
content: 字幕内容
|
||||
|
||||
Returns:
|
||||
SHA256 哈希值(64 字符的十六进制字符串)
|
||||
|
||||
Examples:
|
||||
>>> hash1 = compute_content_hash("相同内容")
|
||||
>>> hash2 = compute_content_hash("相同内容")
|
||||
>>> hash1 == hash2
|
||||
True
|
||||
>>> len(hash1)
|
||||
64
|
||||
"""
|
||||
return hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
class SemanticCache:
|
||||
"""语义分段结果缓存管理器
|
||||
|
||||
使用内容哈希(SHA256)作为缓存键,存储在本地文件系统。
|
||||
|
||||
缓存格式:
|
||||
- 文件命名: {sha256_hash}.json
|
||||
- 文件内容: JSON 格式的 SegmentationResult + version 字段
|
||||
- 缓存 TTL: 永久有效,手动清理
|
||||
|
||||
Attributes:
|
||||
CACHE_VERSION: 缓存格式版本,用于未来兼容性
|
||||
DEFAULT_CACHE_DIR: 默认缓存目录
|
||||
|
||||
Examples:
|
||||
>>> cache = SemanticCache()
|
||||
>>> result = cache.get(subtitle_content)
|
||||
>>> if result is None:
|
||||
... result = llm_segment(subtitle_content)
|
||||
... cache.set(subtitle_content, result)
|
||||
"""
|
||||
|
||||
CACHE_VERSION = "1.0"
|
||||
DEFAULT_CACHE_DIR = Path(".planning/cache/semantic")
|
||||
|
||||
def __init__(self, cache_dir: Optional[Path] = None):
|
||||
"""初始化缓存管理器
|
||||
|
||||
Args:
|
||||
cache_dir: 缓存目录路径,默认为 .planning/cache/semantic/
|
||||
"""
|
||||
self.cache_dir = cache_dir or self.DEFAULT_CACHE_DIR
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def get(self, content: str) -> Optional[SegmentationResult]:
|
||||
"""从缓存读取分段结果
|
||||
|
||||
Args:
|
||||
content: 字幕内容
|
||||
|
||||
Returns:
|
||||
缓存的 SegmentationResult,如果不存在或读取失败则返回 None
|
||||
|
||||
Examples:
|
||||
>>> cache = SemanticCache()
|
||||
>>> result = cache.get("字幕内容")
|
||||
>>> if result:
|
||||
... print(f"从缓存读取: {result.video_type}")
|
||||
"""
|
||||
cache_key = compute_content_hash(content)
|
||||
cache_file = self.cache_dir / f"{cache_key}.json"
|
||||
|
||||
if not cache_file.exists():
|
||||
self.logger.debug(f"Cache miss: {cache_key}")
|
||||
return None
|
||||
|
||||
try:
|
||||
data = json.loads(cache_file.read_text(encoding='utf-8'))
|
||||
|
||||
# 验证版本
|
||||
if data.get('version') != self.CACHE_VERSION:
|
||||
self.logger.warning(
|
||||
f"Cache version mismatch: {data.get('version')} != {self.CACHE_VERSION}"
|
||||
)
|
||||
return None
|
||||
|
||||
# 移除 version 字段,转换为 SegmentationResult
|
||||
data.pop('version', None)
|
||||
result = SegmentationResult(**data)
|
||||
|
||||
self.logger.debug(f"Cache hit: {cache_key}")
|
||||
return result
|
||||
|
||||
except (json.JSONDecodeError, IOError, ValidationError) as e:
|
||||
self.logger.warning(f"Failed to load cache: {e}")
|
||||
return None
|
||||
|
||||
def set(self, content: str, result: SegmentationResult) -> None:
|
||||
"""保存分段结果到缓存
|
||||
|
||||
Args:
|
||||
content: 字幕内容
|
||||
result: 分段结果
|
||||
|
||||
Examples:
|
||||
>>> cache = SemanticCache()
|
||||
>>> result = SegmentationResult(video_type="知识类", segments=[], actual_segment_count=0)
|
||||
>>> cache.set("字幕内容", result)
|
||||
"""
|
||||
cache_key = compute_content_hash(content)
|
||||
cache_file = self.cache_dir / f"{cache_key}.json"
|
||||
|
||||
try:
|
||||
# 确保目录存在
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 序列化为 JSON,添加 version 字段
|
||||
data = result.model_dump()
|
||||
data['version'] = self.CACHE_VERSION
|
||||
|
||||
cache_file.write_text(
|
||||
json.dumps(data, ensure_ascii=False, indent=2),
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
self.logger.debug(f"Cache saved: {cache_key}")
|
||||
|
||||
except IOError as e:
|
||||
self.logger.warning(f"Failed to save cache: {e}")
|
||||
@@ -0,0 +1,853 @@
|
||||
"""语义分段引擎
|
||||
|
||||
使用 LLM 实现智能视频字幕语义分段。
|
||||
|
||||
功能:
|
||||
- 视频类型识别(知识类/故事类/访谈类/新闻类/vlog 等)
|
||||
- 语义边界检测
|
||||
- 内容完整性保护
|
||||
- 主题标题生成
|
||||
- 可扩展的分段策略架构
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import List, Optional, Protocol
|
||||
|
||||
import requests.exceptions
|
||||
from pydantic import BaseModel, Field, field_validator, ValidationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# AI Client Protocol
|
||||
# ============================================================================
|
||||
|
||||
class AIClientProtocol(Protocol):
|
||||
"""AI 客户端协议(与 ASRCorrector 保持一致)"""
|
||||
|
||||
def generate(self, prompt: str, max_tokens: int = 16000,
|
||||
disable_reasoning: bool = False) -> str:
|
||||
"""生成文本"""
|
||||
...
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Segmentation Strategy Pattern (Plan 13-06)
|
||||
# ============================================================================
|
||||
|
||||
class SegmentationStrategy(Protocol):
|
||||
"""分段策略接口
|
||||
|
||||
定义了分段策略的通用接口,支持未来扩展不同的分段策略。
|
||||
|
||||
Examples:
|
||||
>>> strategy = UnifiedSegmentationStrategy()
|
||||
>>> segments = strategy.segment(subtitle_segments, metadata)
|
||||
|
||||
Note:
|
||||
遵守用户决策:Phase 13 实现统一策略,不区分视频类型。
|
||||
"""
|
||||
|
||||
def segment(
|
||||
self,
|
||||
segments: List,
|
||||
metadata: dict
|
||||
) -> List[Union[str, 'VideoSegment']]:
|
||||
"""对字幕分段进行语义分段
|
||||
|
||||
Args:
|
||||
segments: 字幕分段列表(来自 parser.py 的 Segment 对象)
|
||||
metadata: 视频元数据(title, duration 等)
|
||||
|
||||
Returns:
|
||||
分段列表(可以是纯文本列表或 VideoSegment 对象列表)
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class UnifiedSegmentationStrategy:
|
||||
"""统一分段策略
|
||||
|
||||
适用于所有视频类型(知识类、故事类、访谈类等)。
|
||||
视频类型仅用于 Prompt 上下文,不改变分段策略。
|
||||
|
||||
遵守用户决策:无类型区分,使用统一 Prompt。
|
||||
|
||||
Attributes:
|
||||
ai_client: AI 客户端(实现 AIClientProtocol)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ai_client: Optional[AIClientProtocol] = None,
|
||||
fallback_segmenter: Optional['SemanticSegmenter'] = None,
|
||||
cache: Optional['SemanticCache'] = None
|
||||
):
|
||||
"""初始化统一分段策略
|
||||
|
||||
Args:
|
||||
ai_client: AI 客户端实例,如果为 None 则尝试创建 SimpleAIClient
|
||||
fallback_segmenter: 已弃用,保留仅为向后兼容
|
||||
cache: 缓存管理器,如果为 None 则不使用缓存
|
||||
"""
|
||||
self.ai_client = ai_client
|
||||
self.fallback_segmenter = fallback_segmenter
|
||||
self.cache = cache
|
||||
|
||||
# Lazy AI Client 初始化(与 ASRCorrector 相同模式)
|
||||
if self.ai_client is None:
|
||||
try:
|
||||
from kb_workflow.deep_analyzer import SimpleAIClient
|
||||
self.ai_client = SimpleAIClient()
|
||||
if not self.ai_client.api_key:
|
||||
logger.info("AI API key not configured")
|
||||
self.ai_client = None
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to initialize AI client: {e}")
|
||||
self.ai_client = None
|
||||
|
||||
@staticmethod
|
||||
def _is_recoverable_error(error: Exception) -> bool:
|
||||
"""判断错误是否可恢复
|
||||
|
||||
可恢复错误:网络错误、超时、限流、服务器错误(500/502/503/504)、
|
||||
JSON 解析错误(JSONDecodeError, 非 ValidationError 的 ValueError)
|
||||
不可恢复错误:其他 HTTP 错误(4xx)
|
||||
|
||||
注意:Pydantic ValidationError(如标题超长)现在被视为可恢复错误,
|
||||
因为这是LLM生成内容的问题,应该允许重试。
|
||||
|
||||
Args:
|
||||
error: 异常对象
|
||||
|
||||
Returns:
|
||||
True 如果错误可恢复,False 否则
|
||||
"""
|
||||
# 注意:ValidationError 现在被视为可恢复错误
|
||||
# 因为这可能是LLM偶然生成的内容问题,重试可能解决
|
||||
|
||||
# 网络层错误(可恢复)
|
||||
if isinstance(error, requests.exceptions.ConnectionError):
|
||||
return True
|
||||
if isinstance(error, requests.exceptions.Timeout):
|
||||
return True
|
||||
|
||||
# JSON 解析错误(可恢复 - 可能是 LLM 输出格式问题)
|
||||
if isinstance(error, (json.JSONDecodeError, ValueError)):
|
||||
return True
|
||||
|
||||
# HTTP 错误(根据状态码判断)
|
||||
if isinstance(error, requests.exceptions.HTTPError):
|
||||
status_code = error.response.status_code if hasattr(error, 'response') and error.response else None
|
||||
if status_code in (429, 500, 502, 503, 504):
|
||||
return True
|
||||
# 其他 4xx 错误不可恢复
|
||||
return False
|
||||
|
||||
# 其他错误不可恢复
|
||||
return False
|
||||
|
||||
def segment(
|
||||
self,
|
||||
segments: List,
|
||||
metadata: dict
|
||||
) -> List['VideoSegment']:
|
||||
"""对字幕分段进行语义分段
|
||||
|
||||
使用统一的 LLM Prompt 进行语义分段,不区分视频类型。
|
||||
视频类型仅用于 Prompt 上下文。
|
||||
|
||||
Args:
|
||||
segments: 字幕分段列表(来自 parser.py 的 Segment 对象)
|
||||
metadata: 视频元数据(title, duration 等)
|
||||
|
||||
Returns:
|
||||
VideoSegment 对象列表(包含标题和内容)
|
||||
"""
|
||||
# AI 不可用,直接报错
|
||||
if self.ai_client is None:
|
||||
raise RuntimeError(
|
||||
"AI客户端未配置,无法进行LLM语义分段。请检查AI_API_KEY环境变量。"
|
||||
)
|
||||
|
||||
# 合并字幕文本
|
||||
content = " ".join(seg.text for seg in segments)
|
||||
|
||||
# 检查缓存(Plan 14-05: 缓存集成)
|
||||
if self.cache is not None:
|
||||
cached_result = self.cache.get(content)
|
||||
if cached_result is not None:
|
||||
logger.info("Using cached segmentation result")
|
||||
return cached_result.segments
|
||||
|
||||
# 检查是否需要分块(Plan 14-04: 分块阈值 5000 字符)
|
||||
if len(content) <= 5000:
|
||||
# 短文本直接处理
|
||||
video_segments = self._segment_single_chunk(content, metadata, segments)
|
||||
else:
|
||||
# 长文本分块处理
|
||||
video_segments = self._segment_multiple_chunks(content, metadata, segments)
|
||||
|
||||
# 保存到缓存(Plan 14-05: 缓存结果)
|
||||
if self.cache is not None:
|
||||
# 构建完整的 SegmentationResult
|
||||
result = SegmentationResult(
|
||||
video_type=self._infer_video_type(metadata),
|
||||
segments=video_segments,
|
||||
actual_segment_count=len(video_segments)
|
||||
)
|
||||
self.cache.set(content, result)
|
||||
logger.debug("Saved segmentation result to cache")
|
||||
|
||||
return video_segments
|
||||
|
||||
def _build_prompt(self, content: str, metadata: dict) -> str:
|
||||
"""构建语义分段 Prompt
|
||||
|
||||
Args:
|
||||
content: 字幕内容
|
||||
metadata: 视频元数据
|
||||
|
||||
Returns:
|
||||
构建好的 Prompt 字符串
|
||||
"""
|
||||
# 推断视频类型
|
||||
video_type = self._infer_video_type(metadata)
|
||||
title = metadata.get("title", "") or ""
|
||||
|
||||
# 构建 Prompt
|
||||
total_chars = len(content)
|
||||
prompt = f"""你是一个专业的视频内容分析师。请将以下字幕内容按语义分段。
|
||||
|
||||
【视频信息】
|
||||
类型:{video_type}
|
||||
标题:{title}
|
||||
总字数:{total_chars}字
|
||||
|
||||
【字幕内容】
|
||||
{content}
|
||||
|
||||
【分段要求】
|
||||
1. 建议将内容分为 5-8 个段落(可根据语义完整性灵活调整)
|
||||
2. 每个段落约 1000-2000 字(为保持语义完整性可以突破此范围)
|
||||
3. 在话题转换处创建新分段,不要混入不同话题的内容
|
||||
4. **重要**:保持句子完整性,分段边界应放在自然停顿处(句号。、问号?、感叹号!、省略号……等标点符号后)
|
||||
- 基于语义停顿判断,不仅依赖标点符号
|
||||
- 保持内容的连贯性和可读性
|
||||
5. 保持论点完整性,不要在前提-结论结构中间截断
|
||||
6. 为主题/章节类型的分段生成简洁标题(最多30字),论点/观点类型分段不加标题
|
||||
7. **广告识别**:识别UP主的广告植入内容(包括但不限于"转转""京东""得物""淘宝""拼多多"等电商平台推广,以及其他明显的商业推广内容)
|
||||
- 将广告内容单独分为一个段落
|
||||
- 广告段落的 title 必须设为"广告"或"推广"
|
||||
- 不要将广告内容混入正常内容段落中
|
||||
|
||||
【输出格式】
|
||||
必须返回以下 JSON 格式(不要添加任何额外文本):
|
||||
```json
|
||||
{{
|
||||
"video_type": "{video_type}",
|
||||
"segments": [
|
||||
{{
|
||||
"title": "分段标题(如果此分段不需要标题则设为 null)",
|
||||
"content": "分段内容",
|
||||
"start_index": 0,
|
||||
"end_index": 123
|
||||
}}
|
||||
],
|
||||
"actual_segment_count": 6
|
||||
}}
|
||||
```
|
||||
|
||||
【注意事项】
|
||||
- 优先保证语义完整性,不要为了凑数而强行截断
|
||||
- 标题应简洁准确,如「机器学习概述」「梯度下降原理」
|
||||
- 如果某个分段无法生成合适的标题(如过渡性内容),将 title 设为 null
|
||||
- **广告识别提示**:常见的广告植入特征包括:
|
||||
* 突然话题转换到商品推荐
|
||||
* 提及"回收""卖""优惠""下单"等电商相关词汇
|
||||
* 提及具体品牌名称(如转转、京东、得物等)
|
||||
* 说话风格明显变化(如口语化的推销语气)
|
||||
* 内容与视频主题无关的商品推荐"""
|
||||
return prompt
|
||||
|
||||
def _infer_video_type(self, metadata: dict) -> str:
|
||||
"""从元数据推断视频类型
|
||||
|
||||
Args:
|
||||
metadata: 视频元数据,包含 title, category, tags 等字段
|
||||
|
||||
Returns:
|
||||
推断出的视频类型,默认为"知识类"
|
||||
|
||||
Examples:
|
||||
>>> strategy = UnifiedSegmentationStrategy()
|
||||
>>> strategy._infer_video_type({"title": "Python教程"})
|
||||
'知识类'
|
||||
>>> strategy._infer_video_type({"category": "影视"})
|
||||
'故事类'
|
||||
"""
|
||||
if not metadata:
|
||||
return DEFAULT_VIDEO_TYPE
|
||||
|
||||
# 提取所有文本字段
|
||||
title = metadata.get("title", "") or ""
|
||||
category = metadata.get("category", "") or ""
|
||||
tags = metadata.get("tags", []) or []
|
||||
description = metadata.get("description", "") or ""
|
||||
|
||||
# 合并所有文本用于匹配
|
||||
all_text = " ".join([title, category, description] + list(tags))
|
||||
|
||||
# 如果没有有效文本,返回默认类型
|
||||
if not all_text or all_text.isspace():
|
||||
return DEFAULT_VIDEO_TYPE
|
||||
|
||||
# 按优先级匹配类型关键词
|
||||
for video_type, keywords in VIDEO_TYPE_KEYWORDS.items():
|
||||
for keyword in keywords:
|
||||
if keyword in all_text:
|
||||
logger.debug(
|
||||
f"Detected video type '{video_type}' "
|
||||
f"from keyword '{keyword}'"
|
||||
)
|
||||
return video_type
|
||||
|
||||
# 未匹配到任何类型,返回默认
|
||||
logger.debug("No specific video type detected, using default")
|
||||
return DEFAULT_VIDEO_TYPE
|
||||
|
||||
def _parse_result(
|
||||
self,
|
||||
llm_output: str,
|
||||
fallback_segments: List
|
||||
) -> List[VideoSegment]:
|
||||
"""解析 LLM 输出并验证
|
||||
|
||||
Args:
|
||||
llm_output: LLM 原始输出
|
||||
fallback_segments: 已弃用参数,保留仅为向后兼容
|
||||
|
||||
Returns:
|
||||
VideoSegment 对象列表
|
||||
|
||||
Raises:
|
||||
json.JSONDecodeError: JSON 解析失败
|
||||
ValueError: 无法提取 JSON
|
||||
ValidationError: Pydantic 验证失败
|
||||
"""
|
||||
# 提取 JSON
|
||||
data = extract_json_from_llm_response(llm_output)
|
||||
|
||||
# Pydantic 验证
|
||||
result = SegmentationResult(**data)
|
||||
|
||||
logger.info(
|
||||
f"LLM 分段成功: 类型={result.video_type}, "
|
||||
f"分段数={result.actual_segment_count}"
|
||||
)
|
||||
|
||||
return result.segments
|
||||
|
||||
def _segment_single_chunk(
|
||||
self,
|
||||
content: str,
|
||||
metadata: dict,
|
||||
fallback_segments: List
|
||||
) -> List[VideoSegment]:
|
||||
"""处理单个文本块(不分块)
|
||||
|
||||
Args:
|
||||
content: 字幕内容
|
||||
metadata: 视频元数据
|
||||
fallback_segments: 降级时使用的原始分段
|
||||
|
||||
Returns:
|
||||
VideoSegment 对象列表
|
||||
"""
|
||||
# 构建 Prompt
|
||||
prompt = self._build_prompt(content=content, metadata=metadata)
|
||||
|
||||
# 调用 LLM,带重试逻辑(最多 2 次尝试)
|
||||
max_attempts = 2
|
||||
last_error = None
|
||||
last_llm_output = None
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
llm_output = self.ai_client.generate(
|
||||
prompt,
|
||||
max_tokens=8000,
|
||||
disable_reasoning=True # 分段不需要思考模式
|
||||
)
|
||||
|
||||
# 解析和验证
|
||||
video_segments = self._parse_result(
|
||||
llm_output,
|
||||
fallback_segments=fallback_segments
|
||||
)
|
||||
|
||||
logger.info(f"LLM semantic segmentation: {len(video_segments)} segments")
|
||||
return video_segments
|
||||
|
||||
except Exception as e:
|
||||
is_recoverable = self._is_recoverable_error(e)
|
||||
is_last_attempt = (attempt == max_attempts - 1)
|
||||
|
||||
# 可恢复错误且不是最后一次尝试:重试
|
||||
if is_recoverable and not is_last_attempt:
|
||||
logger.info(
|
||||
f"LLM call failed (attempt {attempt + 1}/{max_attempts}): "
|
||||
f"{type(e).__name__}: {e}, retrying..."
|
||||
)
|
||||
continue
|
||||
|
||||
# 最后一次尝试或不可恢复错误:直接抛出异常
|
||||
error_type = type(e).__name__
|
||||
|
||||
# 为 HTTPError 提取更详细的信息
|
||||
if isinstance(e, requests.exceptions.HTTPError) and hasattr(e, 'response') and e.response:
|
||||
status_code = e.response.status_code
|
||||
response_text = getattr(e.response, 'text', '') or ''
|
||||
error_msg = f"HTTP {status_code}"
|
||||
if response_text and isinstance(response_text, str):
|
||||
error_msg += f" ({response_text[:100]})"
|
||||
raise RuntimeError(
|
||||
f"{error_type}: {error_msg}, LLM语义分段失败"
|
||||
) from e
|
||||
else:
|
||||
error_msg = str(e) if str(e) else error_type
|
||||
raise RuntimeError(
|
||||
f"{error_type}: {error_msg}, LLM语义分段失败"
|
||||
) from e
|
||||
|
||||
def _segment_multiple_chunks(
|
||||
self,
|
||||
content: str,
|
||||
metadata: dict,
|
||||
fallback_segments: List
|
||||
) -> List[VideoSegment]:
|
||||
"""处理多个文本块(分块)
|
||||
|
||||
Args:
|
||||
content: 字幕内容
|
||||
metadata: 视频元数据
|
||||
fallback_segments: 降级时使用的原始分段
|
||||
|
||||
Returns:
|
||||
VideoSegment 对象列表
|
||||
"""
|
||||
logger.info(f"Content too long ({len(content)} chars), splitting into chunks")
|
||||
|
||||
# 分块
|
||||
chunks = self._split_into_chunks(content, max_chunk_size=5000)
|
||||
logger.info(f"Split into {len(chunks)} chunks")
|
||||
|
||||
# 处理每个分块
|
||||
chunk_results = []
|
||||
for i, (chunk_text, offset) in enumerate(chunks, 1):
|
||||
logger.info(f"Processing chunk {i}/{len(chunks)} (offset={offset}, length={len(chunk_text)})")
|
||||
|
||||
# 构建该分块的 prompt
|
||||
chunk_metadata = metadata.copy()
|
||||
chunk_metadata['chunk_info'] = f"第 {i}/{len(chunks)} 块"
|
||||
prompt = self._build_prompt(content=chunk_text, metadata=chunk_metadata)
|
||||
|
||||
try:
|
||||
# 调用 LLM
|
||||
llm_output = self.ai_client.generate(prompt, max_tokens=8000, disable_reasoning=True)
|
||||
segments = self._parse_result(llm_output, fallback_segments=fallback_segments)
|
||||
chunk_results.append((segments, offset))
|
||||
logger.info(f"Chunk {i} segmented successfully: {len(segments)} segments")
|
||||
except Exception as e:
|
||||
# 分块处理失败,直接抛出异常
|
||||
raise RuntimeError(
|
||||
f"分块 {i}/{len(chunks)} LLM语义分段失败: {type(e).__name__}: {e}"
|
||||
) from e
|
||||
|
||||
# 合并结果
|
||||
return self._merge_segment_results(chunk_results)
|
||||
|
||||
def _split_into_chunks(self, text: str, max_chunk_size: int = 5000) -> List[tuple[str, int]]:
|
||||
"""按句子边界分割文本为多个块
|
||||
|
||||
Args:
|
||||
text: 待分割的文本
|
||||
max_chunk_size: 每块的最大字符数
|
||||
|
||||
Returns:
|
||||
List of (chunk_text, offset) 元组,offset 是该块在原文中的起始位置
|
||||
"""
|
||||
# 按句子分割
|
||||
sentences = []
|
||||
current = ""
|
||||
for char in text:
|
||||
current += char
|
||||
if char in '。!?\n':
|
||||
sentences.append(current)
|
||||
current = ""
|
||||
|
||||
# 添加最后一个不完整的句子
|
||||
if current:
|
||||
sentences.append(current)
|
||||
|
||||
# 分块
|
||||
chunks = []
|
||||
current_chunk = []
|
||||
current_length = 0
|
||||
current_offset = 0
|
||||
|
||||
for sentence in sentences:
|
||||
sentence_len = len(sentence)
|
||||
if current_length + sentence_len > max_chunk_size:
|
||||
if current_chunk:
|
||||
chunk_text = ''.join(current_chunk)
|
||||
chunks.append((chunk_text, current_offset))
|
||||
current_offset += len(chunk_text)
|
||||
current_chunk = [sentence]
|
||||
current_length = sentence_len
|
||||
else:
|
||||
current_chunk.append(sentence)
|
||||
current_length += sentence_len
|
||||
|
||||
# 添加最后一个块
|
||||
if current_chunk:
|
||||
chunk_text = ''.join(current_chunk)
|
||||
chunks.append((chunk_text, current_offset))
|
||||
|
||||
return chunks
|
||||
|
||||
def _merge_segment_results(
|
||||
self,
|
||||
chunk_results: List[tuple[List[VideoSegment], int]]
|
||||
) -> List[VideoSegment]:
|
||||
"""合并分块的分段结果
|
||||
|
||||
Args:
|
||||
chunk_results: List of (segments, offset) 元组
|
||||
|
||||
Returns:
|
||||
合并后的分段列表,修正 start_index 和 end_index 为相对于原文的位置
|
||||
"""
|
||||
all_segments = []
|
||||
|
||||
for segments, offset in chunk_results:
|
||||
for seg in segments:
|
||||
# 修正索引为相对于原文的位置
|
||||
corrected_seg = VideoSegment(
|
||||
title=seg.title,
|
||||
content=seg.content,
|
||||
start_index=seg.start_index + offset,
|
||||
end_index=seg.end_index + offset
|
||||
)
|
||||
all_segments.append(corrected_seg)
|
||||
|
||||
return all_segments
|
||||
|
||||
def _fallback_chunk(self, chunk_text: str, offset: int) -> List[VideoSegment]:
|
||||
"""对单个分块使用规则分段(已弃用)
|
||||
|
||||
此方法已弃用,不再被调用。如被调用将抛出异常。
|
||||
|
||||
Args:
|
||||
chunk_text: 分块文本
|
||||
offset: 该分块在原文中的起始位置
|
||||
|
||||
Returns:
|
||||
VideoSegment 对象列表
|
||||
|
||||
Raises:
|
||||
RuntimeError: 此方法已弃用
|
||||
"""
|
||||
raise RuntimeError("_fallback_chunk 方法已弃用,LLM语义分段失败将直接抛出异常")
|
||||
|
||||
# 转换为 VideoSegment,并设置正确的偏移量
|
||||
segments = []
|
||||
current_pos = offset
|
||||
for para in paragraphs:
|
||||
# 计算该分段在原文中的位置
|
||||
seg = VideoSegment(
|
||||
title=None,
|
||||
content=para,
|
||||
start_index=current_pos,
|
||||
end_index=current_pos + len(para)
|
||||
)
|
||||
segments.append(seg)
|
||||
current_pos += len(para)
|
||||
|
||||
return segments
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Pydantic Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def extract_json_from_llm_response(response: str) -> dict:
|
||||
"""从 LLM 响应中提取 JSON
|
||||
|
||||
处理以下情况:
|
||||
- 纯 JSON
|
||||
- 包裹在 ```json ``` 中的 JSON
|
||||
- 前后包含额外说明文本的 JSON
|
||||
|
||||
Args:
|
||||
response: LLM 响应文本
|
||||
|
||||
Returns:
|
||||
解析后的 JSON 字典
|
||||
|
||||
Raises:
|
||||
ValueError: 无法提取有效 JSON
|
||||
"""
|
||||
# 尝试直接解析
|
||||
try:
|
||||
return json.loads(response.strip())
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# 尝试提取 ```json ``` 代码块
|
||||
json_match = re.search(r'```json\s*(.*?)\s*```', response, re.DOTALL)
|
||||
if json_match:
|
||||
try:
|
||||
return json.loads(json_match.group(1))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# 尝试提取任何 JSON 对象
|
||||
json_match = re.search(r'\{.*\}', response, re.DOTALL)
|
||||
if json_match:
|
||||
try:
|
||||
return json.loads(json_match.group())
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"无法从 LLM 响应中提取有效 JSON: {e}")
|
||||
|
||||
raise ValueError("LLM 响应中未找到 JSON 内容")
|
||||
|
||||
class VideoSegment(BaseModel):
|
||||
"""视频分段
|
||||
|
||||
Attributes:
|
||||
title: 分段标题(可选)
|
||||
content: 分段内容(必填)
|
||||
start_index: 起始字幕索引
|
||||
end_index: 结束字幕索引
|
||||
"""
|
||||
title: Optional[str] = Field(
|
||||
None,
|
||||
description="分段标题,简洁描述型(最多30字)"
|
||||
)
|
||||
content: str = Field(
|
||||
...,
|
||||
description="分段内容,合并后的字幕文本"
|
||||
)
|
||||
start_index: int = Field(
|
||||
...,
|
||||
ge=0,
|
||||
description="在原文中的起始字符位置"
|
||||
)
|
||||
end_index: int = Field(
|
||||
...,
|
||||
ge=0,
|
||||
description="在原文中的结束字符位置"
|
||||
)
|
||||
|
||||
@field_validator('title')
|
||||
@classmethod
|
||||
def validate_title_length(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""验证标题长度(最多30字)"""
|
||||
if v is not None and len(v) > 30:
|
||||
raise ValueError(f"标题长度不能超过30字,当前为{len(v)}字")
|
||||
return v
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"examples": [
|
||||
{
|
||||
"title": "机器学习概述",
|
||||
"content": "机器学习是人工智能的一个分支...",
|
||||
"start_index": 1,
|
||||
"end_index": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SegmentationResult(BaseModel):
|
||||
"""分段结果
|
||||
|
||||
Attributes:
|
||||
video_type: 视频类型(知识类/故事类/访谈类/新闻类/vlog 等)
|
||||
segments: 分段列表
|
||||
actual_segment_count: 实际分段数量
|
||||
"""
|
||||
video_type: str = Field(
|
||||
...,
|
||||
description="识别出的视频类型"
|
||||
)
|
||||
segments: List[VideoSegment] = Field(
|
||||
default_factory=list,
|
||||
description="分段列表"
|
||||
)
|
||||
actual_segment_count: int = Field(
|
||||
...,
|
||||
ge=0,
|
||||
description="实际分段数量"
|
||||
)
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"examples": [
|
||||
{
|
||||
"video_type": "知识类",
|
||||
"segments": [
|
||||
{
|
||||
"title": "机器学习概述",
|
||||
"content": "...",
|
||||
"start_index": 1,
|
||||
"end_index": 10
|
||||
}
|
||||
],
|
||||
"actual_segment_count": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Video Type Detection
|
||||
# ============================================================================
|
||||
|
||||
# 视频类型关键词映射
|
||||
VIDEO_TYPE_KEYWORDS = {
|
||||
"知识类": [
|
||||
"教程", "课程", "教学", "入门", "基础", "进阶", "实战",
|
||||
"编程", "开发", "技术", "学习", "教育", "培训", "讲座"
|
||||
],
|
||||
"故事类": [
|
||||
"影视", "电影", "电视剧", "动漫", "动画", "小说", "故事",
|
||||
"剧情", "短片", "纪录片"
|
||||
],
|
||||
"访谈类": [
|
||||
"访谈", "对话", "采访", "专访", "聊天", "脱口秀", "播客"
|
||||
],
|
||||
"新闻类": [
|
||||
"新闻", "报道", "资讯", "时事", "热点", "快讯"
|
||||
],
|
||||
"vlog": [
|
||||
"vlog", "日常", "生活", "旅行", "美食", "探店",
|
||||
"分享", "体验"
|
||||
]
|
||||
}
|
||||
|
||||
# 默认视频类型
|
||||
DEFAULT_VIDEO_TYPE = "知识类"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Semantic Segmentation Engine
|
||||
# ============================================================================
|
||||
|
||||
class SemanticSegmentationEngine:
|
||||
"""语义分段引擎(使用策略架构)
|
||||
|
||||
使用策略模式实现可扩展的分段架构。
|
||||
|
||||
Attributes:
|
||||
strategy: 分段策略(实现 SegmentationStrategy 接口)
|
||||
|
||||
Examples:
|
||||
>>> # 使用默认统一策略
|
||||
>>> engine = SemanticSegmentationEngine()
|
||||
>>> paragraphs = engine.segment(segments, metadata)
|
||||
|
||||
>>> # 使用自定义策略(未来扩展)
|
||||
>>> custom_strategy = KnowledgeTypeStrategy()
|
||||
>>> engine = SemanticSegmentationEngine(strategy=custom_strategy)
|
||||
>>> paragraphs = engine.segment(segments, metadata)
|
||||
|
||||
Note:
|
||||
Plan 13-06: 实现可扩展策略架构,Phase 13 使用统一策略。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
strategy: Optional[SegmentationStrategy] = None,
|
||||
fallback_segmenter: Optional['SemanticSegmenter'] = None,
|
||||
cache: Optional['SemanticCache'] = None
|
||||
):
|
||||
"""初始化语义分段引擎
|
||||
|
||||
Args:
|
||||
strategy: 分段策略实例,如果为 None 则使用统一策略
|
||||
fallback_segmenter: 降级时使用的规则分段器(已弃用,保留兼容性)
|
||||
cache: 缓存管理器
|
||||
"""
|
||||
# 默认使用统一策略
|
||||
if strategy is None:
|
||||
self.strategy = UnifiedSegmentationStrategy(
|
||||
fallback_segmenter=fallback_segmenter,
|
||||
cache=cache
|
||||
)
|
||||
else:
|
||||
self.strategy = strategy
|
||||
|
||||
logger.debug(
|
||||
f"SemanticSegmentationEngine initialized with strategy: "
|
||||
f"{self.strategy.__class__.__name__}"
|
||||
)
|
||||
|
||||
def _infer_video_type(self, metadata: dict) -> str:
|
||||
"""从元数据推断视频类型(代理到策略)
|
||||
|
||||
Args:
|
||||
metadata: 视频元数据
|
||||
|
||||
Returns:
|
||||
推断出的视频类型
|
||||
"""
|
||||
# 如果策略有 _infer_video_type 方法,使用它
|
||||
if hasattr(self.strategy, '_infer_video_type'):
|
||||
return self.strategy._infer_video_type(metadata)
|
||||
# 否则返回默认类型
|
||||
return DEFAULT_VIDEO_TYPE
|
||||
|
||||
def _build_prompt(self, content: str, metadata: dict) -> str:
|
||||
"""构建 Prompt(代理到策略)
|
||||
|
||||
Args:
|
||||
content: 字幕内容
|
||||
metadata: 视频元数据
|
||||
|
||||
Returns:
|
||||
构建好的 Prompt
|
||||
"""
|
||||
# 如果策略有 _build_prompt 方法,使用它
|
||||
if hasattr(self.strategy, '_build_prompt'):
|
||||
return self.strategy._build_prompt(content, metadata)
|
||||
# 否则返回基本 prompt
|
||||
return f"Content: {content}\nMetadata: {metadata}"
|
||||
|
||||
def segment(
|
||||
self,
|
||||
segments: List,
|
||||
metadata: dict
|
||||
) -> List['VideoSegment']:
|
||||
"""对字幕分段进行语义分段
|
||||
|
||||
使用当前配置的策略进行分段。
|
||||
|
||||
Args:
|
||||
segments: 字幕分段列表(来自 parser.py 的 Segment 对象)
|
||||
metadata: 视频元数据(title, duration 等)
|
||||
|
||||
Returns:
|
||||
VideoSegment 对象列表(包含标题和内容)
|
||||
"""
|
||||
return self.strategy.segment(segments, metadata)
|
||||
Reference in New Issue
Block a user