npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@speclip/pi-media

v0.3.4

Published

Workspace-safe media analysis, editing, rendering, review, and stock-footage sourcing tools for Pi

Readme

@speclip/pi-media

Pi 用的视频处理工具包。让 AI Agent 能安全地取材、分析、裁剪、渲染视频,并且每一个成片都能说清自己是从哪来的

它解决什么问题

让 Agent 直接调 ffmpeg 命令有两个麻烦:一是容易误伤——覆盖原文件、写到工作区外面去;二是出了片没法追溯——这个 MP4 到底是从哪个素材、剪了哪一段来的,事后说不清。

这个包的做法是:编辑不改文件,只记账。

你的原始素材自始至终不会被动。所有"想怎么剪"的意图,都记录成工作区里 .media/ 目录下一份份不可变的说明书(叫快照)。渲染时照着某一份说明书生成一个文件,同时留下一张凭证。最后验收时,顺着凭证倒推回去核对,对不上就不通过。

环境要求

  • Node.js 22.19+
  • macOS 或 Linux(渲染链路依赖 POSIX 文件描述符,不支持 Windows)
  • Pi 0.85.x
  • PATH 里能找到 ffprobeffmpeg
  • media_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-libonnxruntimednn_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_importpexels_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.json0.1.0 时使用 v0.1.0。工作流会检出 Release 标签,依次执行 npm cinpm run checknpm pack --dry-run,全部通过后通过 npm Trusted Publishing(OIDC)发布公开包 @speclip/pi-media,不读取长期 npm Token。正式 Release 发布到 latest,Prerelease 发布到 next

在 npm 包设置中一次性配置 Trusted Publisher:

  1. Provider 选择 GitHub Actions。
  2. Organization/User 填写 linyqh,Repository 填写 pi-media
  3. Workflow filename 填写 publish.yml,Allowed actions 启用 npm publish

每次发布前同时更新 package.jsonpackage-lock.json 的版本并提交,然后创建标签与版本完全匹配的 GitHub Release。