vidknot 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vidknot-0.1.0/LICENSE +21 -0
- vidknot-0.1.0/PKG-INFO +40 -0
- vidknot-0.1.0/README.md +1 -0
- vidknot-0.1.0/pyproject.toml +68 -0
- vidknot-0.1.0/setup.cfg +4 -0
- vidknot-0.1.0/src/vidknot/__init__.py +25 -0
- vidknot-0.1.0/src/vidknot/__main__.py +275 -0
- vidknot-0.1.0/src/vidknot/adapters/__init__.py +13 -0
- vidknot-0.1.0/src/vidknot/adapters/agent_bridge.py +147 -0
- vidknot-0.1.0/src/vidknot/adapters/feishu_writer.py +444 -0
- vidknot-0.1.0/src/vidknot/adapters/mcp_server.py +386 -0
- vidknot-0.1.0/src/vidknot/adapters/notion_writer.py +239 -0
- vidknot-0.1.0/src/vidknot/adapters/obsidian_writer.py +276 -0
- vidknot-0.1.0/src/vidknot/adapters/yuque_writer.py +147 -0
- vidknot-0.1.0/src/vidknot/api.py +192 -0
- vidknot-0.1.0/src/vidknot/core/__init__.py +15 -0
- vidknot-0.1.0/src/vidknot/core/cookie_provider.py +346 -0
- vidknot-0.1.0/src/vidknot/core/douyin_parser.py +220 -0
- vidknot-0.1.0/src/vidknot/core/download_manager.py +319 -0
- vidknot-0.1.0/src/vidknot/core/downloader.py +773 -0
- vidknot-0.1.0/src/vidknot/core/processor.py +303 -0
- vidknot-0.1.0/src/vidknot/core/transcriber.py +157 -0
- vidknot-0.1.0/src/vidknot/pipeline/__init__.py +7 -0
- vidknot-0.1.0/src/vidknot/pipeline/video_knowledge_pipeline.py +249 -0
- vidknot-0.1.0/src/vidknot/utils/__init__.py +74 -0
- vidknot-0.1.0/src/vidknot/utils/cache_manager.py +165 -0
- vidknot-0.1.0/src/vidknot/utils/config_manager.py +304 -0
- vidknot-0.1.0/src/vidknot/utils/env_check.py +157 -0
- vidknot-0.1.0/src/vidknot/utils/exceptions.py +152 -0
- vidknot-0.1.0/src/vidknot/utils/logger.py +124 -0
- vidknot-0.1.0/src/vidknot.egg-info/PKG-INFO +40 -0
- vidknot-0.1.0/src/vidknot.egg-info/SOURCES.txt +37 -0
- vidknot-0.1.0/src/vidknot.egg-info/dependency_links.txt +1 -0
- vidknot-0.1.0/src/vidknot.egg-info/entry_points.txt +2 -0
- vidknot-0.1.0/src/vidknot.egg-info/requires.txt +18 -0
- vidknot-0.1.0/src/vidknot.egg-info/top_level.txt +1 -0
- vidknot-0.1.0/tests/test_config_manager.py +168 -0
- vidknot-0.1.0/tests/test_exceptions.py +124 -0
- vidknot-0.1.0/tests/test_logger.py +97 -0
vidknot-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VidkNot Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
vidknot-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vidknot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: VidkNot — Video Knowledge, Knotted. 一键将视频链接转换为结构化笔记,支持飞书、语雀、Notion、Obsidian 等多平台存储。
|
|
5
|
+
Author: VidkNot Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/suonian/vidknot
|
|
8
|
+
Project-URL: Repository, https://github.com/suonian/vidknot
|
|
9
|
+
Project-URL: Issues, https://github.com/suonian/vidknot/issues
|
|
10
|
+
Keywords: video,transcription,AI,notes,knowledge-base,feishu,yuque,notion,obsidian,markdown
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: yt-dlp>=2026.3.17
|
|
24
|
+
Requires-Dist: fastapi>=0.136.0
|
|
25
|
+
Requires-Dist: uvicorn[standard]>=0.27.0
|
|
26
|
+
Requires-Dist: httpx>=0.27.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
28
|
+
Requires-Dist: openai>=1.0.0
|
|
29
|
+
Provides-Extra: feishu
|
|
30
|
+
Requires-Dist: feishu-docx>=0.1.0; extra == "feishu"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.2.0; extra == "dev"
|
|
34
|
+
Provides-Extra: all
|
|
35
|
+
Requires-Dist: feishu-docx>=0.1.0; extra == "all"
|
|
36
|
+
Requires-Dist: pytest>=8.0.0; extra == "all"
|
|
37
|
+
Requires-Dist: ruff>=0.2.0; extra == "all"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
- 📖 [文档改进](https://github.com/suonian/vidknot/issues)
|
vidknot-0.1.0/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- 📖 [文档改进](https://github.com/suonian/vidknot/issues)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vidknot"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "VidkNot — Video Knowledge, Knotted. 一键将视频链接转换为结构化笔记,支持飞书、语雀、Notion、Obsidian 等多平台存储。"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [{name = "VidkNot Team"}]
|
|
12
|
+
keywords = ["video", "transcription", "AI", "notes", "knowledge-base", "feishu", "yuque", "notion", "obsidian", "markdown"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
23
|
+
]
|
|
24
|
+
requires-python = ">=3.10"
|
|
25
|
+
dependencies = [
|
|
26
|
+
"yt-dlp>=2026.3.17",
|
|
27
|
+
"fastapi>=0.136.0",
|
|
28
|
+
"uvicorn[standard]>=0.27.0",
|
|
29
|
+
"httpx>=0.27.0",
|
|
30
|
+
"pyyaml>=6.0.1",
|
|
31
|
+
"openai>=1.0.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
feishu = ["feishu-docx>=0.1.0"]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8.0.0",
|
|
38
|
+
"ruff>=0.2.0",
|
|
39
|
+
]
|
|
40
|
+
all = [
|
|
41
|
+
"feishu-docx>=0.1.0",
|
|
42
|
+
"pytest>=8.0.0",
|
|
43
|
+
"ruff>=0.2.0",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.scripts]
|
|
47
|
+
vidknot = "vidknot.__main__:main"
|
|
48
|
+
|
|
49
|
+
[project.urls]
|
|
50
|
+
Homepage = "https://github.com/suonian/vidknot"
|
|
51
|
+
Repository = "https://github.com/suonian/vidknot"
|
|
52
|
+
Issues = "https://github.com/suonian/vidknot/issues"
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.packages.find]
|
|
55
|
+
where = ["src"]
|
|
56
|
+
|
|
57
|
+
[tool.setuptools.package-data]
|
|
58
|
+
vidknot = ["*.yaml", "*.yml"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 100
|
|
62
|
+
target-version = "py310"
|
|
63
|
+
select = ["E", "F", "I", "N", "W", "UP"]
|
|
64
|
+
ignore = ["E501"]
|
|
65
|
+
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
testpaths = ["tests"]
|
|
68
|
+
addopts = "-v --tb=short"
|
vidknot-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VidkNot - Video Knowledge, Knotted.
|
|
3
|
+
|
|
4
|
+
Tie your video knowledge together.
|
|
5
|
+
视频知识,结成一网。
|
|
6
|
+
|
|
7
|
+
Core module for Agent-driven video knowledge pipeline.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__author__ = "VidkNot Team"
|
|
12
|
+
__license__ = "MIT"
|
|
13
|
+
|
|
14
|
+
from .core.downloader import VideoDownloader
|
|
15
|
+
from .core.transcriber import SiliconFlowASR, get_transcriber
|
|
16
|
+
from .core.processor import ContentProcessor
|
|
17
|
+
from .pipeline.video_knowledge_pipeline import VideoKnowledgePipeline
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"VideoDownloader",
|
|
21
|
+
"SiliconFlowASR",
|
|
22
|
+
"get_transcriber",
|
|
23
|
+
"ContentProcessor",
|
|
24
|
+
"VideoKnowledgePipeline",
|
|
25
|
+
]
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
VidkNot 统一入口
|
|
4
|
+
|
|
5
|
+
支持三种运行模式:
|
|
6
|
+
- CLI: python -m vidknot <url> [--destination feishu|yuque|notion|obsidian|both|none]
|
|
7
|
+
- MCP: python -m vidknot --mcp
|
|
8
|
+
- FastAPI: uvicorn vidknot.api:app
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import argparse
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .utils.logger import get_logger
|
|
16
|
+
|
|
17
|
+
logger = get_logger("vidknot.cli")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
"""智能检测运行模式并启动对应服务"""
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
description="VidkNot — Video Knowledge, Knotted. 将视频链接转换为结构化笔记,自动保存到飞书文档或 Obsidian Vault。",
|
|
24
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
25
|
+
epilog="""
|
|
26
|
+
运行模式:
|
|
27
|
+
CLI 模式: python -m vidknot <url> [--destination feishu|yuque|notion|obsidian|both|none]
|
|
28
|
+
MCP 模式: python -m vidknot --mcp
|
|
29
|
+
API 模式: uvicorn vidknot.api:app --reload
|
|
30
|
+
|
|
31
|
+
示例:
|
|
32
|
+
# 生成笔记并保存到 Obsidian (默认)
|
|
33
|
+
python -m vidknot https://youtube.com/watch?v=xxx
|
|
34
|
+
|
|
35
|
+
# 保存到飞书文档
|
|
36
|
+
python -m vidknot https://bilibili.com/video/BVxxx --destination feishu
|
|
37
|
+
|
|
38
|
+
# 仅返回笔记内容,不保存
|
|
39
|
+
python -m vidknot https://youtube.com/watch?v=xxx --destination none
|
|
40
|
+
|
|
41
|
+
# 启动 MCP Server (用于 OpenClaw / Claude Desktop)
|
|
42
|
+
python -m vidknot --mcp
|
|
43
|
+
"""
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# 运行模式参数
|
|
47
|
+
parser.add_argument("--mcp", action="store_true",
|
|
48
|
+
help="启动 MCP Stdio 模式 (用于 OpenClaw / Claude Desktop)")
|
|
49
|
+
parser.add_argument("--cli", action="store_true",
|
|
50
|
+
help="强制使用 CLI 模式 (默认自动检测)")
|
|
51
|
+
parser.add_argument("--check-env", action="store_true",
|
|
52
|
+
help="检查运行环境依赖是否满足")
|
|
53
|
+
|
|
54
|
+
# CLI 参数
|
|
55
|
+
parser.add_argument("url", nargs="?",
|
|
56
|
+
help="视频 URL (CLI 模式)")
|
|
57
|
+
parser.add_argument("--raw", action="store_true",
|
|
58
|
+
help="仅输出原始转录文本")
|
|
59
|
+
parser.add_argument("--summary", action="store_true",
|
|
60
|
+
help="生成结构化笔记 (默认)")
|
|
61
|
+
parser.add_argument("--destination", "-d", default="obsidian",
|
|
62
|
+
choices=["feishu", "yuque", "notion", "obsidian", "both", "none"],
|
|
63
|
+
help="笔记保存目的地: feishu(飞书)/yuque(语雀)/notion/obsidian(本地)/both(所有)/none(仅返回)")
|
|
64
|
+
parser.add_argument("--language", "-l", default="auto",
|
|
65
|
+
help="视频语言: auto/zh/en/ja/ko (默认: auto)")
|
|
66
|
+
parser.add_argument("--output", "-o",
|
|
67
|
+
help="输出文件路径 (当 destination=none 时)")
|
|
68
|
+
parser.add_argument("--no-cache", action="store_true",
|
|
69
|
+
help="禁用缓存")
|
|
70
|
+
parser.add_argument("--feishu-folder",
|
|
71
|
+
help="飞书文档保存的文件夹名称")
|
|
72
|
+
parser.add_argument("--obsidian-tags", nargs="+", default=[],
|
|
73
|
+
help="Obsidian 笔记标签")
|
|
74
|
+
parser.add_argument("--notify", action="store_true", default=True,
|
|
75
|
+
help="处理完成后发送通知 (通过 OpenClaw 消息回复)")
|
|
76
|
+
|
|
77
|
+
args = parser.parse_args()
|
|
78
|
+
|
|
79
|
+
# 模式检测优先级: check-env > MCP > CLI (有url) > Help
|
|
80
|
+
if args.check_env:
|
|
81
|
+
from .utils.env_check import check_all_requirements, get_install_guide
|
|
82
|
+
all_ok, messages = check_all_requirements()
|
|
83
|
+
for msg in messages:
|
|
84
|
+
print(msg)
|
|
85
|
+
if not all_ok:
|
|
86
|
+
print("\n" + get_install_guide())
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
print("\n[OK] 所有环境检查通过")
|
|
89
|
+
sys.exit(0)
|
|
90
|
+
|
|
91
|
+
if args.mcp:
|
|
92
|
+
# MCP Stdio 模式
|
|
93
|
+
from .adapters.mcp_server import run_mcp_server
|
|
94
|
+
run_mcp_server()
|
|
95
|
+
|
|
96
|
+
elif args.url or args.cli:
|
|
97
|
+
# CLI 模式
|
|
98
|
+
if not args.url and args.cli:
|
|
99
|
+
parser.error("CLI 模式需要提供 URL 参数")
|
|
100
|
+
run_cli(args)
|
|
101
|
+
|
|
102
|
+
else:
|
|
103
|
+
# 检查是否通过 uvicorn 调用 (FastAPI 模式)
|
|
104
|
+
if "uvicorn" in sys.argv[0] or any("uvicorn" in arg for arg in sys.argv):
|
|
105
|
+
print("启动 FastAPI 模式,请访问 http://localhost:8000/docs")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
parser.print_help()
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def run_cli(args):
|
|
113
|
+
"""CLI 模式主逻辑(同步)"""
|
|
114
|
+
from .core.downloader import VideoDownloader
|
|
115
|
+
from .core.transcriber import SiliconFlowASR
|
|
116
|
+
from .core.processor import ContentProcessor
|
|
117
|
+
from .utils.cache_manager import CacheManager
|
|
118
|
+
from .utils.env_check import check_ffmpeg, check_all_requirements
|
|
119
|
+
from .pipeline.video_knowledge_pipeline import VideoKnowledgePipeline
|
|
120
|
+
|
|
121
|
+
logger.info("VidkNot v0.1.0 — Video Knowledge, Knotted.")
|
|
122
|
+
|
|
123
|
+
# 环境检查
|
|
124
|
+
logger.info("检查运行环境...")
|
|
125
|
+
all_ok, messages = check_all_requirements()
|
|
126
|
+
if not all_ok:
|
|
127
|
+
for msg in messages:
|
|
128
|
+
if "❌" in msg:
|
|
129
|
+
logger.error(msg)
|
|
130
|
+
logger.error("请安装缺失的依赖后重试")
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
|
|
133
|
+
ffmpeg_ok, ffmpeg_path = check_ffmpeg()
|
|
134
|
+
logger.info(f"FFmpeg: {ffmpeg_path}")
|
|
135
|
+
|
|
136
|
+
# 确定模式
|
|
137
|
+
mode = "raw" if args.raw else "summary"
|
|
138
|
+
destination = args.destination
|
|
139
|
+
|
|
140
|
+
url = args.url
|
|
141
|
+
logger.info(f"正在处理: {url}")
|
|
142
|
+
logger.info(f"处理模式: {'结构化笔记' if mode == 'summary' else '原始转录'}")
|
|
143
|
+
logger.info(f"目的地: {destination}")
|
|
144
|
+
|
|
145
|
+
# 检查缓存
|
|
146
|
+
cache = CacheManager()
|
|
147
|
+
if not args.no_cache:
|
|
148
|
+
cached = cache.get(url, mode)
|
|
149
|
+
if cached:
|
|
150
|
+
logger.info("命中缓存!")
|
|
151
|
+
result = cached
|
|
152
|
+
else:
|
|
153
|
+
result = process_video(url, mode, args.language)
|
|
154
|
+
cache.set(url, mode, result)
|
|
155
|
+
else:
|
|
156
|
+
result = process_video(url, mode, args.language)
|
|
157
|
+
|
|
158
|
+
# 根据目的地路由
|
|
159
|
+
if destination != "none":
|
|
160
|
+
pipeline = VideoKnowledgePipeline(
|
|
161
|
+
destination=destination,
|
|
162
|
+
format=mode,
|
|
163
|
+
language=args.language,
|
|
164
|
+
)
|
|
165
|
+
saved = pipeline.save(result, {
|
|
166
|
+
"feishu_folder": args.feishu_folder,
|
|
167
|
+
"obsidian_tags": args.obsidian_tags,
|
|
168
|
+
})
|
|
169
|
+
logger.info(f"已保存到: {saved}")
|
|
170
|
+
|
|
171
|
+
# 输出结果
|
|
172
|
+
if mode == "summary":
|
|
173
|
+
output = result.get("markdown", "")
|
|
174
|
+
else:
|
|
175
|
+
output = result.get("transcription", "")
|
|
176
|
+
|
|
177
|
+
if args.output:
|
|
178
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
179
|
+
f.write(output)
|
|
180
|
+
logger.info(f"结果已保存: {args.output}")
|
|
181
|
+
else:
|
|
182
|
+
print("\n" + "=" * 60)
|
|
183
|
+
print(output)
|
|
184
|
+
print("=" * 60)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def generate_images_markdown(metadata: dict, image_paths: list) -> str:
|
|
188
|
+
"""为纯图片内容生成 Markdown"""
|
|
189
|
+
from datetime import date
|
|
190
|
+
|
|
191
|
+
title = metadata.get("title", "小红书图片笔记")
|
|
192
|
+
url = metadata.get("url", "")
|
|
193
|
+
image_dir = metadata.get("image_paths", [])
|
|
194
|
+
|
|
195
|
+
lines = [
|
|
196
|
+
"---",
|
|
197
|
+
f"title: {title}",
|
|
198
|
+
f"source: {url}",
|
|
199
|
+
f"date: {date.today().isoformat()}",
|
|
200
|
+
f"platform: xiaohongshu",
|
|
201
|
+
f"type: images",
|
|
202
|
+
"---",
|
|
203
|
+
"",
|
|
204
|
+
f"# {title}",
|
|
205
|
+
"",
|
|
206
|
+
f"来源: {url}",
|
|
207
|
+
"",
|
|
208
|
+
f"图片数量: {len(image_paths)} 张",
|
|
209
|
+
"",
|
|
210
|
+
"## 图片列表",
|
|
211
|
+
"",
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
for i, img_path in enumerate(image_paths, 1):
|
|
215
|
+
from pathlib import Path
|
|
216
|
+
filename = Path(img_path).name
|
|
217
|
+
lines.append(f"### 图片 {i}")
|
|
218
|
+
lines.append(f"")
|
|
219
|
+
lines.append("")
|
|
220
|
+
|
|
221
|
+
lines.append("---")
|
|
222
|
+
lines.append(f"*图片保存目录: {image_dir[0].rsplit('/', 1)[0] if image_dir else '未知'}*")
|
|
223
|
+
|
|
224
|
+
return "\n".join(lines)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def process_video(url: str, mode: str, language: str) -> dict:
|
|
228
|
+
"""处理视频的完整流程(同步)"""
|
|
229
|
+
from .core.downloader import VideoDownloader
|
|
230
|
+
from .core.transcriber import SiliconFlowASR
|
|
231
|
+
from .core.processor import ContentProcessor
|
|
232
|
+
|
|
233
|
+
logger.info("正在下载...")
|
|
234
|
+
downloader = VideoDownloader()
|
|
235
|
+
file_path, metadata = downloader.download_audio_with_metadata(url)
|
|
236
|
+
logger.info(f"下载完成: {metadata.get('title', 'Unknown')}")
|
|
237
|
+
|
|
238
|
+
result = {
|
|
239
|
+
"title": metadata.get("title"),
|
|
240
|
+
"author": metadata.get("uploader"),
|
|
241
|
+
"duration": metadata.get("duration"),
|
|
242
|
+
"source_url": url,
|
|
243
|
+
"transcription": "",
|
|
244
|
+
"metadata": metadata,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if metadata.get("is_images_only"):
|
|
248
|
+
image_count = metadata.get("image_count", 0)
|
|
249
|
+
image_paths = metadata.get("image_paths", [])
|
|
250
|
+
result["transcription"] = ""
|
|
251
|
+
result["markdown"] = generate_images_markdown(metadata, image_paths)
|
|
252
|
+
result["is_images_only"] = True
|
|
253
|
+
logger.info(f"纯图片笔记: {image_count} 张图片")
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
logger.info("正在转录...")
|
|
257
|
+
transcriber = SiliconFlowASR()
|
|
258
|
+
transcription = transcriber.transcribe(file_path, language=language)
|
|
259
|
+
logger.info(f"转录完成: {len(transcription)} 字符")
|
|
260
|
+
result["transcription"] = transcription
|
|
261
|
+
|
|
262
|
+
if mode == "summary":
|
|
263
|
+
logger.info("正在生成结构化笔记...")
|
|
264
|
+
processor = ContentProcessor()
|
|
265
|
+
processed = processor.summarize(transcription, metadata)
|
|
266
|
+
result["markdown"] = processed["markdown"]
|
|
267
|
+
logger.info("笔记生成完成")
|
|
268
|
+
else:
|
|
269
|
+
result["markdown"] = transcription
|
|
270
|
+
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
if __name__ == "__main__":
|
|
275
|
+
main()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VidkNot Agent 桥接模块
|
|
3
|
+
|
|
4
|
+
提供 OpenAI Function Calling Schema 定义
|
|
5
|
+
用于 OpenAI / Claude 等 AI Agent 集成
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
from typing import Dict, Any, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_tool_metadata() -> Dict[str, Any]:
|
|
14
|
+
"""
|
|
15
|
+
获取 VidkNot 工具的 OpenAI Function Calling Schema
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
OpenAI tool schema dict
|
|
19
|
+
"""
|
|
20
|
+
return {
|
|
21
|
+
"type": "function",
|
|
22
|
+
"function": {
|
|
23
|
+
"name": "video_knowledge",
|
|
24
|
+
"description": """将视频链接转换为结构化笔记,并自动保存到飞书文档或 Obsidian Vault。
|
|
25
|
+
|
|
26
|
+
适用于以下场景:
|
|
27
|
+
- 用户转发视频链接后,AI Agent 自动生成学习笔记
|
|
28
|
+
- 将视频内容整理为可搜索的 Markdown 笔记
|
|
29
|
+
- 自动提取视频要点、章节、关键信息
|
|
30
|
+
|
|
31
|
+
支持的平台:YouTube、Bilibili、抖音、小红书、微博、Twitter/X 等 20+ 平台
|
|
32
|
+
|
|
33
|
+
输出:结构化的 Markdown 笔记,包含 YAML Frontmatter(标题、作者、时长、标签等)""",
|
|
34
|
+
"parameters": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"url": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "视频链接,支持 YouTube、Bilibili、抖音、小红书、微博、Twitter/X 等平台"
|
|
40
|
+
},
|
|
41
|
+
"destination": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": ["feishu", "obsidian", "both", "none"],
|
|
44
|
+
"description": "笔记保存目的地:feishu=飞书云文档,obsidian=本地 Obsidian Vault,both=两者同时,none=仅返回内容",
|
|
45
|
+
"default": "obsidian"
|
|
46
|
+
},
|
|
47
|
+
"format": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"enum": ["structured", "raw"],
|
|
50
|
+
"description": "structured=结构化笔记(含要点、详细内容、标签),raw=仅原始转录",
|
|
51
|
+
"default": "structured"
|
|
52
|
+
},
|
|
53
|
+
"language": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "视频语言:auto(自动检测)/ zh / en / ja / ko",
|
|
56
|
+
"default": "auto"
|
|
57
|
+
},
|
|
58
|
+
"feishu_folder": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "飞书文档保存的文件夹名称(如:视频笔记、学习资料)"
|
|
61
|
+
},
|
|
62
|
+
"obsidian_tags": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"items": {"type": "string"},
|
|
65
|
+
"description": "Obsidian 笔记标签(自动添加到 YAML Frontmatter 的 tags 字段)"
|
|
66
|
+
},
|
|
67
|
+
"notify": {
|
|
68
|
+
"type": "boolean",
|
|
69
|
+
"description": "处理完成后是否发送通知(通过 Agent 消息回复用户)",
|
|
70
|
+
"default": True
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"required": ["url"]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def execute_tool(arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
80
|
+
"""
|
|
81
|
+
执行 video_knowledge 工具
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
arguments: 工具参数
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
工具执行结果
|
|
88
|
+
"""
|
|
89
|
+
from ..pipeline.video_knowledge_pipeline import VideoKnowledgePipeline
|
|
90
|
+
|
|
91
|
+
url = arguments.get("url")
|
|
92
|
+
destination = arguments.get("destination", "obsidian")
|
|
93
|
+
format_mode = arguments.get("format", "structured")
|
|
94
|
+
language = arguments.get("language", "auto")
|
|
95
|
+
feishu_folder = arguments.get("feishu_folder")
|
|
96
|
+
obsidian_tags = arguments.get("obsidian_tags", [])
|
|
97
|
+
notify = arguments.get("notify", True)
|
|
98
|
+
|
|
99
|
+
if not url:
|
|
100
|
+
return {
|
|
101
|
+
"success": False,
|
|
102
|
+
"error": "URL is required"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
pipeline = VideoKnowledgePipeline(
|
|
107
|
+
destination=destination,
|
|
108
|
+
format=format_mode,
|
|
109
|
+
language=language,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
result = asyncio.run(pipeline.run(url))
|
|
113
|
+
|
|
114
|
+
if destination != "none":
|
|
115
|
+
saved = asyncio.run(
|
|
116
|
+
pipeline.save(result, {
|
|
117
|
+
"feishu_folder": feishu_folder,
|
|
118
|
+
"obsidian_tags": obsidian_tags,
|
|
119
|
+
})
|
|
120
|
+
)
|
|
121
|
+
result["saved_to"] = saved
|
|
122
|
+
|
|
123
|
+
if notify:
|
|
124
|
+
dest_label = {
|
|
125
|
+
"feishu": "飞书文档",
|
|
126
|
+
"obsidian": "Obsidian Vault",
|
|
127
|
+
"both": "飞书文档和 Obsidian",
|
|
128
|
+
}.get(destination, destination)
|
|
129
|
+
result["notify_message"] = f"✅ 笔记已生成并保存到 {dest_label}!"
|
|
130
|
+
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
# 分类异常,返回结构化错误
|
|
135
|
+
from ..utils.exceptions import VidkNotError
|
|
136
|
+
if isinstance(e, VidkNotError):
|
|
137
|
+
return {
|
|
138
|
+
"success": False,
|
|
139
|
+
"error": e.message,
|
|
140
|
+
"error_details": e.details,
|
|
141
|
+
"error_type": e.__class__.__name__,
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
"success": False,
|
|
145
|
+
"error": str(e),
|
|
146
|
+
"error_type": e.__class__.__name__,
|
|
147
|
+
}
|