@speclip/pi-media
Workspace-safe media analysis, editing, rendering, review, and stock-footage sourcing tools for Pi
Package details
Install @speclip/pi-media from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@speclip/pi-media- Package
@speclip/pi-media- Version
0.3.4- Published
- Sep 5, 2026
- Downloads
- 894/mo · 894/wk
- Author
- tapcli
- License
- unknown
- Types
- extension, skill, prompt
- Size
- 30.1 MB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./extensions/media-local/index.ts",
"./extensions/media-cloud/index.ts"
],
"skills": [
"./skills"
],
"prompts": [
"./prompts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@speclip/pi-media
给 Pi 用的视频处理工具包。让 AI Agent 能安全地取材、分析、裁剪、渲染视频,并且每一个成片都能说清自己是从哪来的。
它解决什么问题
让 Agent 直接调 ffmpeg 命令有两个麻烦:一是容易误伤——覆盖原文件、写到工作区外面去;二是出了片没法追溯——这个 MP4 到底是从哪个素材、剪了哪一段来的,事后说不清。
这个包的做法是:编辑不改文件,只记账。
你的原始素材自始至终不会被动。所有"想怎么剪"的意图,都记录成工作区里 .media/ 目录下一份份不可变的说明书(叫快照)。渲染时照着某一份说明书生成一个新文件,同时留下一张凭证。最后验收时,顺着凭证倒推回去核对,对不上就不通过。
环境要求
- Node.js 22.19+
- macOS 或 Linux(渲染链路依赖 POSIX 文件描述符,不支持 Windows)
- Pi 0.85.x
PATH里能找到ffprobe和ffmpegmedia_scene_detect额外要求 FFmpeg 9.0+,且构建时启用--enable-libonnxruntime并包含dnn_processing;仅版本号正确还不够
安装
npm install
npm run check
pi install ./
安装后会得到两个 Extension,边界按是否需要凭证划分:
media-local— 9 个工具,不需要任何 API key。视频探测、联系表、TransNetV2 镜头检测、编辑、渲染、验收,外加一个下载公共 HTTPS 素材的asset_import。media-cloud— 需要配置云服务 API key 才能用的工具,目前是 Pexels 图库的搜索与下载。没配 key 或用不上时,建议用pi config关掉它。
注意这条边界划的是「要不要凭证」而不是「联不联网」——asset_import 待在 local 但它确实会联网。两边的下载走的是同一套加固过的下载器,详见安全设计。
Pi Extension 是以你当前用户的权限运行的,Pi 的"项目可信"机制不是安全沙箱。能关的边界就关掉。
完整走一遍
假设你有 raw/interview.mp4,想剪出第 10 秒到第 40 秒交付。
1. 看清楚素材是什么
media_probe { path: "raw/interview.mp4" }
返回时长、容器格式、每条视频/音频流的编码和参数,外加文件的 SHA-256 指纹和字节数。
指纹是整套设计的地基——有了它,后面每一步都能确认"我处理的还是当初那个文件吗"。
需要让 Agent 通过视觉选择 BGM 片段时,生成音频分析图:
media_audio_analyze {
path: "assets/bgm.wav",
outputDir: "analysis/bgm"
}
输出的 1920×520 PNG 包含波形、短时 LUFS 曲线和 HH:MM:SS.mmm 绝对时间轴;manifest 使用 EBU R128 / ITU-R BS.1770 记录 Integrated LUFS、LRA、True Peak、来源指纹和每张图片指纹。分析最终口播时可传 ranges,按成片顺序只测保留的 A-roll 区间。
如果需要先快速理解视频内容,可以在建档前生成联系表:
media_contact_sheet {
path: "raw/interview.mp4",
outputDir: "analysis/interview-storyboards",
precision: "medium",
segmentSeconds: 20,
sampling: { mode: "fps", framesPerSecond: 1 }
}
工具把代表帧排成固定 1920×1440(4:3)的故事板,列数会根据帧密度和横屏/竖屏方向自动计算,不再固定为 3 列;缩略帧低于可读阈值时会自动分页。每行下方都有固定 64px 高的局部音频波形,每张 PNG 都带帧编号、页码和 HH:MM:SS.mmm 绝对时间戳。PNG 本身不能携带可播放音轨,波形是从源音频提取的可视证据。输出目录还包含 contact_sheet_manifest.json,记录源视频 SHA-256、抽帧时间点、分页布局和每张图片的 SHA-256。
precision 必须由 Agent 根据任务选择:
| 精度 | 默认分段 | 抽帧密度 | 适用场景 |
|---|---|---|---|
low |
60 秒 | 15 个均匀帧,可按强转场补到最多 30 帧 | 长视频初筛、快速总览、优先速度 |
medium |
20 秒 | 固定每秒 1 帧(每段 20 帧) | 常规剧情理解,速度与覆盖平衡 |
high |
10 秒 | 每秒 2 个均匀帧,可按强转场补到最多每秒 5 帧 | 快切、动作密集、证据敏感或结果存在歧义 |
可用 segmentSeconds 调整每段时长;sampling 可选择 { mode: "count", framesPerSegment } 精确指定完整分段的帧数(末尾不足一段时按时长等比缩减),或 { mode: "fps", framesPerSecond } 指定均匀采样率。两种模式都不能超过 5fps。显式设置 sampling 后不再额外补强转场帧;省略时使用上表的自适应策略。输出目录必须是新路径,已有目录不会被覆盖。长视频可用 startSeconds / endSeconds 限定分析范围。
如果任务需要可直接用于精确切片的镜头边界,而不只是联系表里的启发式补帧,运行 TransNetV2:
media_scene_detect {
path: "raw/interview.mp4",
outputPath: "analysis/interview-scenes.json",
threshold: 0.5
}
它把固定在包内的 transnetv2-ffmpeg v0.1.0 ONNX 模型交给 FFmpeg 9.0+ 的 dnn_processing / ONNX Runtime 后端,返回合并后的切镜点、单帧与 all-frame 概率,以及首尾相接的 scene ranges。peakFrame 是模型概率峰所在的旧镜头末帧,boundaryFrame 是下一镜头首帧,切片应使用后者。报告包含源文件和模型 SHA-256,并以新 JSON 文件落盘;已有文件绝不覆盖。
这个工具会同时检查版本、--enable-libonnxruntime 和 dnn_processing。普通 Homebrew/apt 安装即使显示 FFmpeg 9,也可能没带 ONNX 后端;不满足时错误会指出实际检测结果,并链接到已验证的 build_ffmpeg_9.sh。构建好后把兼容版 ffmpeg 放进 PATH,或设置:
export PI_MEDIA_FFMPEG_BINARY=/absolute/path/to/ffmpeg
ffprobe 默认仍从 PATH 读取;需要单独指定时使用 PI_MEDIA_FFPROBE_BINARY。
2. 建一个档案
project_create { projectId: "interview-cut", sourcePath: "raw/interview.mp4" }
在 .media/projects/interview-cut/ 下建档,并直接写入 revision 1:一份"素材是 interview.mp4(指纹 a3f5…),编辑操作:无"的快照。
revision 1 就是未经编辑的原始状态。
3. 改之前先查当前第几版
project_get { projectId: "interview-cut" }
不传 revision 就返回最新版。为什么必须先查?见下一步。
4. 写下新的一版
edit_apply {
projectId: "interview-cut",
expectedRevision: 1,
operations: [{ kind: "trim", startSeconds: 10, endSeconds: 40 }]
}
需要把多个口播片段或多机位素材拼成一条时间线时,改用通用 EDL:
edit_apply {
projectId: "interview-cut",
expectedRevision: 1,
operations: [{
kind: "timeline",
segments: [
{ id: "hook", sourcePath: "raw/take-1.mp4", sourceStartSeconds: 2.4, sourceEndSeconds: 8.1 },
{ id: "answer", sourcePath: "raw/take-2.mp4", sourceStartSeconds: 11.2, sourceEndSeconds: 24.6 }
],
overlays: [{
id: "product-demo",
sourcePath: "assets/demo.mp4",
outputStartSeconds: 4.0,
outputEndSeconds: 7.5,
sourceStartSeconds: 0,
fit: "cover",
audio: "keep-primary"
}],
subtitleTrack: {
sourcePath: "subtitles/final.ass",
format: "ass",
mode: "burn-in"
},
musicTrack: {
sourcePath: "assets/bgm.wav",
sourceStartSeconds: 12,
sourceEndSeconds: 18,
outputStartSeconds: 0,
outputEndSeconds: 30,
playback: "loop",
fadeInSeconds: 0.5,
fadeOutSeconds: 1.5,
ducking: "dialogue-sidechain",
mix: {
method: "ebu-r128-dialogue-relative",
dialogueAnalysisPath: "analysis/voice/audio_analysis_manifest.json",
musicAnalysisPath: "analysis/bgm-window/audio_analysis_manifest.json",
targetMusicBelowDialogueLu: 16,
maxTruePeakDbtp: -1
}
}
}]
}
segments 的数组顺序就是成片顺序;overlays 使用成片时间轴定位 B-roll,并始终保留主音轨;可选的 subtitleTrack 把审核完成的 SRT/ASS 烧录进画面。musicTrack 不接受音量百分比或手填 LUFS,而是读取口播与最终选中音乐窗口的 EBU R128 manifest,再按目标 LU 差值计算增益;说话时自动 ducking,循环连接处交叉淡化,结尾按指定时长淡出。所有媒体、分析 manifest 和字幕素材在 revision 写入时都会哈希,后续文件被替换就拒绝渲染。
它不改 revision 1,而是新写一份 revision 2。旧版本永远原样保留。
expectedRevision 是乐观并发控制——你在声明"我是基于第 1 版改的"。如果这期间别人已经写到第 2 版了,会直接报冲突让你重读,而不是默默把对方的改动盖掉。这就是第 3 步的意义。
5. 照着某一版出片
render { projectId: "interview-cut", revision: 2, outputPath: "out/final.mp4" }
读 revision 2 的快照,用 ffmpeg 生成 H.264 MP4(源文件有音轨就编 AAC)。过程中实时报进度,可以中途取消,默认 30 分钟超时。
两个硬性行为:
- 目标文件已存在就报错,绝不覆盖。
- 渲染前重新校验源文件指纹。如果
raw/interview.mp4在建档之后被人改过,指纹对不上会直接失败——而不是悄悄拿新素材出片。
同时在 .media/renders/ 下留一张凭证:成片指纹、来自哪个项目的第几版、当时的源素材指纹。
6. 验收
review { path: "out/final.mp4" }
做两件事:
内容检查 — 没有视频轨(error)、时长缺失或非正数(error)、没有音频轨(warning,提醒你确认是不是故意的)。
来源核验 — 拿成片指纹去翻凭证,然后顺着倒推:凭证 → 指向哪个快照 → 那个快照记的源素材指纹 → 对不对得上。任何一环断了都是 error。
只要有一条 error,accepted 就是 false。
你自己用命令行 ffmpeg 剪出来的 MP4,扔给
review一定不通过——因为它没有凭证,追溯不到任何快照。这是故意的。
工具速查
| Extension | 工具 | 干什么 | 需要 key |
|---|---|---|---|
| local | media_probe |
读元数据、流信息、SHA-256、字节数 | 否 |
| local | media_audio_analyze |
生成带绝对时间戳、短时 LUFS 的波形图及 EBU R128 manifest | 否 |
| local | media_contact_sheet |
按 Agent 选择的三级精度生成带局部波形的 PNG 故事板 | 否 |
| local | media_scene_detect |
用 TransNetV2 + FFmpeg 9.0+ 检出精确切镜点并写 JSON 报告 | 否 |
| local | project_create |
建项目,写入 revision 1 | 否 |
| local | project_get |
读项目当前版本号和指定版本的快照 | 否 |
| local | edit_apply |
基于指定版本写新快照:单段裁剪或通用多源 EDL + B-roll/BGM | 否 |
| local | render |
照指定版本渲染新 MP4,留下凭证 | 否 |
| local | review |
内容检查 + 来源核验,返回结构化验收报告 | 否 |
| local | asset_import |
下载公共 HTTPS 素材(需用户逐次确认) | 否 |
| cloud | pexels_search |
搜 Pexels 图库的图片或视频,返回候选与可选清晰度 | 是 |
| cloud | pexels_download |
按 ID 下载某个 Pexels 素材,并记下署名信息 | 是 |
projectId 用小写字母、数字和中间的连字符,1–64 字符。渲染输出必须是 .mp4。
包里还附带一个 edit-video Skill,把上面 6 步固化成流程,Agent 照着走即可。
用 Pexels 找素材
pexels_search 拿候选,pexels_download 按 ID 取回。分两步是为了让你(或 Agent)先看清楚再决定下哪个,而不是靠运气。
pexels_search { query: "ocean waves", mediaType: "video", perPage: 5 }
→ [{ assetId, width, height, durationSeconds, author, pexelsUrl,
previewUrl, variants: [{ width, height, fps, bytes }] }, ...]
pexels_download { mediaType: "video", assetId: 25961000,
targetPath: "assets/ocean.mp4", minWidth: 1920 }
→ 落盘 + 带署名的来源记录
minWidth 会挑满足条件里最小的那档,所以要 1080 不会给你拽个 4K 回来。不填就取最大档。
下载回来的素材,来源信息长这样:
{
"kind": "pexels",
"url": "https://videos.pexels.com/video-files/25961000/1920.mp4",
"assetKind": "video",
"assetId": 25961000,
"pexelsUrl": "https://www.pexels.com/video/a-boat-25961000/",
"author": "Nisasu",
"authorUrl": "https://www.pexels.com/@nisasu-1151927884"
}
署名字段是刻意留的——Pexels 的授权条款要求署名。它会沿 MediaRef → 快照 → 渲染凭证 一路带到成片,所以交付时你随时能查出用了谁的素材。
配 API key
key 不从环境变量读。
在 Speclip 里:设置 → 媒体,粘贴 key 即可。没配 key 时工具抛出的错误里会带一个可点的「打开设置」按钮,直接跳到那一页,不用自己找。
裸 Pi CLI 下:手工创建这个文件。
<agentDir>/media-credentials.json
agentDir 在 Speclip 下是 ~/.speclip/agent/,裸 Pi 下是 ~/.pi/agent/。格式:
{
"schemaVersion": 1,
"providers": {
"pexels": { "apiKey": "你的 key" }
}
}
文件权限必须是 600。 里面是明文 key,所以只要其他用户可读,工具会直接拒绝运行而不是凑合用下去。
chmod 600 ~/.pi/agent/media-credentials.json
pi-media 只读不写这个文件。Pexels 的 key 在 pexels.com/api 免费申请,配额 200 次/小时、20000 次/月。
.media/ 里存了什么
.media/
├── projects/<projectId>/
│ ├── project.json 当前版本号等元信息
│ ├── snapshots/1.json revision 1(原始状态)
│ ├── snapshots/2.json revision 2(剪 10–40s)
│ └── .write-lock 并发写锁
├── renders/<成片SHA256>/
│ └── <路径哈希>.json 渲染凭证
├── probe-inputs/ 探测用临时文件(用完即删)
├── contact-sheet-inputs/ 联系表用私有源快照(打开后立即 unlink)
├── contact-sheet-work/ 抽帧、波形和 PPM 临时文件(用完即删)
├── scene-detection-inputs/ 镜头检测用私有源快照(打开后立即 unlink)
└── render-inputs/ 渲染用临时文件(用完即删)
快照一旦写入就不再修改。想回到旧版本重新出片,直接对着那个 revision 号跑 render 就行。
安全设计
这个包相当大比例的代码在处理边界防护,简单说明动机:
跑不出工作区。 每个路径都做两道检查:先按字符串算出绝对路径看是否越界,再 realpath 解析真实位置复查一遍。状态文件额外用 lstat 拒绝符号链接——防的是有人在 .media/projects/ 底下塞一个指向工作区外的软链。
防"掉包"(TOCTOU)。 probe 和 render 都不直接把路径交给 ffmpeg,而是:先把源文件复制成私有副本 → 打开拿到文件描述符 → 立刻 unlink 断开路径 → 对这个描述符校验哈希 → 只把 /dev/fd/3 交给 ffmpeg。整个过程中 ffmpeg 拿不到任何可以被中途替换的路径。
从不覆盖。 单文件产物先写临时文件,再 link() 到目标位置;联系表则先在临时目录完整生成,再用原子 mkdir() 占用最终目录,逐个移入 PNG,最后写入 manifest 作为完成标志。遇到 EEXIST 就报错;失败回滚前会用 dev/ino 确认目录仍是本次任务创建的目标。
防 SSRF。 所有下载——asset_import 和 pexels_download 都算——走同一个加固过的下载器:强制 HTTPS、拒绝 URL 里带凭证、DNS 解析后逐个 IP 比对私有网段黑名单(IPv4/IPv6 全覆盖)、把解析结果固定给底层请求防 DNS rebinding、重定向每一跳重新校验、最多 5 跳。默认大小上限 100MB(硬顶 500MB),默认超时 60 秒(上限 5 分钟)。Content-Length 会撒谎,所以下载过程中还按实际字节数二次把关。
Pexels 下载额外收紧。 目标主机锁定在 images.pexels.com / videos.pexels.com / static-videos.pexels.com,且完全不允许重定向(实测这些资源本来就是直连响应)。而且下载 URL 是按素材 ID 重新问 API 拿的,不接受调用方传 URL——多一次 API 调用,换掉一整类注入风险。
asset_import 联网必须人工确认。 三重门:参数里 confirmNetworkAccess: true、当前会话有 UI、用户在弹窗点确认。缺一不可,非交互式会话直接拒绝。这个工具不持有任何凭证。
凭证文件权限强制。 读 media-credentials.json 前先查 mode,只要 group 或 other 可读就拒绝运行并提示 chmod 600,不静默降级。同时用 lstat 拒绝符号链接。这个包只读不写该文件。
并发安全。 项目写入用可恢复的 .write-lock 加日志。进程中途被杀不会留下半成品——下次访问时会检测到死锁并从日志续完或回滚。
当前能做什么
0.1 版有意收窄范围,只做一条完整可用的链路:
- 单个源视频片段
- 三级精度的视频联系表、局部音频波形和启发式镜头变化补帧
- TransNetV2 + FFmpeg 9.0+ 精确镜头边界、概率与可切片 scene ranges
- 单段裁剪,或按顺序拼接最多 1000 个跨文件片段的通用 EDL
- 最多 500 个 B-roll 视频覆盖窗口,支持 cover/contain 并保留主音轨
- 输出 H.264 MP4(源有音轨则编 AAC)
- 基于完整性和来源关系的结构化验收
- 从工作区、公共 HTTPS、或 Pexels 图库取得素材
暂不支持:转录、图片覆盖、转场、字幕、混音、响度分析、AI 媒体生成、Pexels 以外的图库、Graph 工作流。语义选段和 B-roll 匹配仍由上层领域插件负责;pi-media 只接收明确 EDL 并做可验证渲染。
设计上也刻意不把 ffmpeg 和 HTTPS 包装成通用底层工具暴露给 Agent——那样就回到最开始的问题了。工作流编排交给 Skill 或 @viccydev/pi-graph 这类独立包。
想把这个包接进 Speclip 桌面端,看 docs/speclip-integration.md。
开发
npm run typecheck # tsc --noEmit
npm test # node --test
npm run check # 上面两个
npm pack --dry-run # 检查打包产物
纯 TypeScript,没有构建步骤——Node 22 用 --experimental-strip-types 直接跑 .ts。零运行时依赖。
测试覆盖这些边界:
- 用真实 Pi 0.84.1 Loader 加载两个 Extension 和全部 11 个工具
- 联系表三级精度、场景补帧、静音源、并发 no-clobber、取消/超时和临时目录清理
- TransNetV2 镜头检测的边界聚合、scene ranges、模型校验、FFmpeg 版本/ONNX 能力预检和 JSON no-clobber
- 目录和状态文件的符号链接逃逸
- 编辑与建项目被中断后的恢复
- 取消、超时、进度反馈
- 联网授权门禁和 SSRF 拒绝
- 凭证文件缺失、损坏、软链、权限过松各自的拒绝路径
- Pexels 下载的主机白名单、
per_page夹紧、HLS 条目过滤、清晰度选档 - 渲染凭证写入失败后只清理本次产物
- 目标已存在时拒绝覆盖
- 伪造凭证被 review 拒绝
- 真实 ffmpeg 跑通 H.264/AAC MP4 和无音频 MPEG-4 AVI 两条链路
Pexels 相关用例全部用打桩的 fetch 跑,不触真实网络,CI 不需要 API key。
兼容性验证还额外手工解压了 npm 打包产物,用 Pi 0.84.2 做过加载冒烟测试。
发布到 npm
发布动作由 GitHub Release 触发。Release 标签必须严格使用 v<package.json version>,例如 package.json 为 0.1.0 时使用 v0.1.0。工作流会检出 Release 标签,依次执行 npm ci、npm run check 和 npm pack --dry-run,全部通过后通过 npm Trusted Publishing(OIDC)发布公开包 @speclip/pi-media,不读取长期 npm Token。正式 Release 发布到 latest,Prerelease 发布到 next。
在 npm 包设置中一次性配置 Trusted Publisher:
- Provider 选择 GitHub Actions。
- Organization/User 填写
linyqh,Repository 填写pi-media。 - Workflow filename 填写
publish.yml,Allowed actions 启用npm publish。
每次发布前同时更新 package.json 和 package-lock.json 的版本并提交,然后创建标签与版本完全匹配的 GitHub Release。