feat: implement obsidian-kb skill with long video chunking
- Multi-source content aggregation (Bilibili/YouTube/Zhihu) - Smart inbox management with AI quality scoring - Long video chunking analysis (30min+ auto-segmentation) - Topic-based organization - Git auto-sync to Obsidian - Template system with Jinja2 and fallback rendering - CLI commands: subscribe, inbox, process, skip, status, search - Comprehensive test suite with unit and integration tests - Documentation: README.md and SKILL.md Checkpoint 7-8 completed by Agent E
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
"""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) # 关键洞察
|
||||
timeline_summary: List[Dict[str, Any]] = field(default_factory=list) # 时间线摘要
|
||||
content_structure: str = "" # 内容结构描述
|
||||
recommended_tags: List[str] = field(default_factory=list) # 推荐标签
|
||||
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()
|
||||
Reference in New Issue
Block a user