08f7c9f26c
- Add extractors layer with BilibiliExtractor (Playwright stealth mode) - Add outputs layer with ObsidianWriter and GitSync - Implement subtitle/no-subtitle graceful degradation - Update config with output and bilibili extraction settings - Complete integration pipeline: extract → process → output → sync - Add comprehensive tests (100% pass rate) - Update documentation (SKILL.md, README.md, requirements.txt)
214 lines
7.2 KiB
Python
214 lines
7.2 KiB
Python
"""Obsidian KB - 知识库内容管理模型"""
|
|
|
|
from enum import Enum
|
|
from dataclasses import dataclass, field, asdict
|
|
from typing import Optional, List, Dict, Any
|
|
from datetime import datetime
|
|
import uuid
|
|
import json
|
|
|
|
|
|
class ContentType(Enum):
|
|
"""内容类型"""
|
|
VIDEO = "video"
|
|
ARTICLE = "article"
|
|
|
|
|
|
class ContentStatus(Enum):
|
|
"""内容处理状态"""
|
|
NEW = "new" # 新发现
|
|
SCANNED = "scanned" # 已扫描元数据
|
|
CONFIRMED = "confirmed" # 已确认处理
|
|
ANALYZED = "analyzed" # 已AI分析
|
|
GENERATED = "generated" # 已生成笔记
|
|
SKIPPED = "skipped" # 已跳过
|
|
|
|
|
|
@dataclass
|
|
class ContentChunk:
|
|
"""内容块 - 用于分段处理"""
|
|
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
|
content: str = "" # 内容文本
|
|
start_time: Optional[int] = None # 开始时间(秒)
|
|
end_time: Optional[int] = None # 结束时间(秒)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> "ContentChunk":
|
|
return cls(**data)
|
|
|
|
|
|
@dataclass
|
|
class ContentAnalysis:
|
|
"""内容分析结果 - 用于模板渲染"""
|
|
one_liner: str = "" # 一句话摘要
|
|
summary: str = "" # 详细摘要
|
|
key_points: List[str] = field(default_factory=list) # 关键要点
|
|
tags: List[str] = field(default_factory=list) # 标签
|
|
quality_score: float = 0.0 # 质量评分 0-100
|
|
|
|
# 教程视频专用字段
|
|
prerequisites: List[str] = field(default_factory=list) # 前置知识
|
|
outline: List[Dict[str, Any]] = field(default_factory=list) # 大纲分段
|
|
pitfalls: List[str] = field(default_factory=list) # 常见坑点
|
|
practice_advice: List[str] = field(default_factory=list) # 实践建议
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> "ContentAnalysis":
|
|
return cls(**data)
|
|
|
|
def is_high_quality(self, threshold: float = 60.0) -> bool:
|
|
"""判断是否高质量内容"""
|
|
return self.quality_score >= threshold
|
|
|
|
|
|
@dataclass
|
|
class QualityMetrics:
|
|
"""内容质量指标"""
|
|
overall_score: float = 0.0 # 综合评分 0-1
|
|
information_density: float = 0.0 # 信息密度
|
|
originality: float = 0.0 # 原创性
|
|
practicality: float = 0.0 # 实用性
|
|
depth: float = 0.0 # 深度
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> "QualityMetrics":
|
|
return cls(**data)
|
|
|
|
|
|
@dataclass
|
|
class AnalysisResult:
|
|
"""AI深度分析结果"""
|
|
main_topics: List[str] = field(default_factory=list) # 主要主题
|
|
key_insights: List[str] = field(default_factory=list) # 关键洞察
|
|
action_items: List[str] = field(default_factory=list) # 行动项
|
|
summary: str = "" # 摘要
|
|
timeline: List[Dict[str, Any]] = field(default_factory=list) # 时间线
|
|
recommended_tags: List[str] = field(default_factory=list) # 推荐标签
|
|
full_analysis: str = "" # 完整分析
|
|
timeline_summary: List[Dict[str, Any]] = field(default_factory=list) # 时间线摘要(兼容字段)
|
|
content_structure: str = "" # 内容结构描述
|
|
target_audience: str = "" # 目标受众
|
|
difficulty_level: str = "intermediate" # 难度级别
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> "AnalysisResult":
|
|
return cls(**data)
|
|
|
|
|
|
@dataclass
|
|
class TimelineEntry:
|
|
"""时间线条目"""
|
|
timestamp: str
|
|
title: str
|
|
description: str
|
|
|
|
def to_dict(self) -> Dict[str, str]:
|
|
return asdict(self)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, str]) -> "TimelineEntry":
|
|
return cls(**data)
|
|
|
|
|
|
@dataclass
|
|
class Content:
|
|
"""内容数据模型"""
|
|
# 基本信息
|
|
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
|
source: str = "" # 来源: bilibili, youtube, zhihu, local
|
|
source_id: str = "" # 来源唯一ID
|
|
url: str = ""
|
|
title: str = ""
|
|
author: str = ""
|
|
description: str = ""
|
|
duration: Optional[int] = None # 视频时长(秒)
|
|
|
|
# 内容类型与状态
|
|
content_type: ContentType = ContentType.VIDEO
|
|
status: ContentStatus = ContentStatus.NEW
|
|
|
|
# 元数据
|
|
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
processed_at: Optional[str] = None
|
|
|
|
# 原始内容
|
|
transcript: str = "" # 字幕/原文
|
|
|
|
# AI分析结果
|
|
summary: str = "" # 摘要
|
|
one_liner: str = "" # 一句话摘要
|
|
key_points: List[str] = field(default_factory=list) # 关键要点
|
|
tags: List[str] = field(default_factory=list) # 标签
|
|
quality_score: float = 0.0 # 质量评分
|
|
analysis: Optional[ContentAnalysis] = None # 详细分析结果
|
|
outline: List[str] = field(default_factory=list) # 大纲
|
|
timeline: List[TimelineEntry] = field(default_factory=list) # 时间线
|
|
quality_metrics: QualityMetrics = field(default_factory=QualityMetrics)
|
|
|
|
# 分类与标签
|
|
category: str = "" # 主题分类
|
|
|
|
# 输出
|
|
output_path: Optional[str] = None # 生成的笔记路径
|
|
note_path: Optional[str] = None # 别名,用于兼容
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""序列化为字典"""
|
|
data = asdict(self)
|
|
data['content_type'] = self.content_type.value
|
|
data['status'] = self.status.value
|
|
data['quality_metrics'] = self.quality_metrics.to_dict()
|
|
data['timeline'] = [e.to_dict() for e in self.timeline]
|
|
return data
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> "Content":
|
|
"""从字典反序列化"""
|
|
# 处理枚举
|
|
data['content_type'] = ContentType(data.get('content_type', 'video'))
|
|
data['status'] = ContentStatus(data.get('status', 'new'))
|
|
|
|
# 处理嵌套对象
|
|
if 'quality_metrics' in data and isinstance(data['quality_metrics'], dict):
|
|
data['quality_metrics'] = QualityMetrics.from_dict(data['quality_metrics'])
|
|
else:
|
|
data['quality_metrics'] = QualityMetrics()
|
|
|
|
if 'timeline' in data and isinstance(data['timeline'], list):
|
|
data['timeline'] = [TimelineEntry.from_dict(e) for e in data['timeline']]
|
|
else:
|
|
data['timeline'] = []
|
|
|
|
return cls(**{k: v for k, v in data.items() if k in [f.name for f in cls.__dataclass_fields__.values()]})
|
|
|
|
def to_json(self) -> str:
|
|
"""序列化为JSON字符串"""
|
|
return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
|
|
|
|
@classmethod
|
|
def from_json(cls, json_str: str) -> "Content":
|
|
"""从JSON字符串反序列化"""
|
|
return cls.from_dict(json.loads(json_str))
|
|
|
|
def is_high_quality(self, threshold: float = 0.7) -> bool:
|
|
"""判断是否高质量内容"""
|
|
return self.quality_metrics.overall_score >= threshold
|
|
|
|
def update_status(self, status: ContentStatus):
|
|
"""更新状态"""
|
|
self.status = status
|
|
if status == ContentStatus.GENERATED:
|
|
self.processed_at = datetime.now().isoformat()
|