OpenReel Video 滤镜预设子系统:基于 LUT 的 60 款滤镜目录设计与配方驱动生成工具链
【免费下载链接】openreel-videoOpenReel Video - Professional browser-based video editor. Open source CapCut alternative. 100% browser-based, no installation, no cloud uploads, no watermarks.项目地址: https://gitcode.com/GitHub_Trending/op/openreel-video
导读
本文围绕 OpenReel Video(开源浏览器端视频编辑器,README.md 所描述的开源 CapCut 替代方案)中"滤镜预设(Filter Presets)"这一子系统的设计规格展开,完整解读 Filter Presets v1 Design Spec。你将掌握:如何用 YAML 配方 + Python 代码生成 33³ 三维查找表(LUT),如何通过 Cloudflare R2 公共域以"无 Worker"方式交付 60 款滤镜,如何在 iOS / Android 上实现 LUT 渲染、0–100% 强度混合与 CapCut 风格的滤镜选择器,以及从既有 ~20 个参数化滤镜预设到 LUT 目录的无损迁移方案。
背景与目标:从参数化滤镜到 LUT 目录
当前 OpenReel 的视频调色能力由约 20 个参数驱动的滤镜预设组成,仓库中的实现位于 packages/core/src/video/filter-presets.ts,以FilterPreset接口描述(id、name、category、description、effects[]),效果类型包括brightness、contrast、saturation、hue、blur、sharpen、vignette、grain等,并作为能力清单通过 packages/core/src/capabilities/manifest.ts 的filterPresets字段对外声明。这类预设由参数实时计算,跨平台渲染难以保证逐通道一致。
Filter Presets v1 的目标是把它升级为经过策展、代码生成的约 60 款基于 LUT 的滤镜目录:
- 60 款滤镜分布在 6 个分类下,无需发布应用即可上线(纯静态内容下发);
- 选择器打开后,暖缓存下首块贴图绘制 ≤ 250 ms;
- 强度滑杆在片段(clip)级别混合原始画面与滤镜输出;
- 跨平台渲染一致性:iOS 与 Android 在相同
(source, filter, intensity)三元组下输出差异 ≤ 每通道 1 LSB; - 引用旧版
filterPreset效果的存量用户项目透明迁移。
明确的非目标(Non-Goals)
设计文档划定了严格边界,避免范围蔓延:
- 晕影(vignette)、颗粒(grain)、模糊(blur)、漏光(light leaks)等无法表达为 3D LUT的程序化效果,归入独立的"程序化特效"规格;
- AR/美颜、瘦身、化妆、面部贴纸等效果;
- 用户导入
.cube文件(推迟到 v2); - "收藏(Favorites)"交互(v1 只有 Recents);
- 项目级全局观感(v1 仅支持片段级);
- 付费/Pro 滤镜层级(本设计不含 entitlement 系统)。
架构总览:三块独立可测的组件
设计采用三层架构,每层边界清晰、可独立测试:
┌──────────────────────────┐ ┌────────────────────────────┐ │ Build-time tool │ │ Cloudflare R2 (public) │ │ ───────────────── │ push │ ────────────────── │ │ scripts/filters/ │ ─────▶ │ R2 bucket: │ │ - recipes/*.yaml │ │ openreel-filters/ │ │ - generate.py │ │ manifest.json │ │ - manifest writer │ │ cube/<id>.cube │ │ │ │ Custom domain: │ │ Outputs: │ │ filters.openreel.video │ │ - 60 × .cube │ │ /manifest.json │ │ - manifest.json │ │ /cube/<id>.cube │ │ │ │ (R2 native ETag + cache) │ └──────────────────────────┘ └────────────┬───────────────┘ │ ▼ ┌────────────────────────────────────────────────┐ │ Mobile (iOS Swift & Android Kotlin) │ │ ────────────────────────────────────── │ │ • FilterCatalogService │ │ • FilterLutCache │ │ • FilterRenderer │ │ • FilterPickerViewModel + view │ │ • Clip model: filterId? + intensity │ └────────────────────────────────────────────────┘三大边界:
- 工具边界:输入为
recipes/*.yaml,输出为.cube+ manifest。这是一个纯函数,配套 golden-file 测试; - 托管边界:R2 公共桶 + Cloudflare 自定义域,ETag 与不可变缓存直接来自 R2,路径上无 Worker;
- 移动端边界:两个平台各实现四个类,表面接口完全一致,每个类只负责一件事。
与现有代码的插槽关系:iOS 的Core/Effects/CubeLUTParser.swift与Core/Rendering/VideoEffectRenderer.swift已能解析.cube并运行 CIFilter 链;Android 的core/effects/CubeLUTParser.kt与core/effects/ClipEffectPipeline.kt是对等物。滤镜交付属于静态内容负载,直接由 R2 服务,仓库中的apps/cloudWorker 保持不动(它单独处理模板/分享/AI,且按仓库约定被 gitignore)。
Recipe 工具链:YAML 配方驱动的 LUT 工厂
目录结构
设计文档规划的工具目录位于仓库根下的scripts/filters/,当前仓库中已实际落地:
scripts/filters/ ├── generate.py # 主工具(入口) ├── transforms.py # 单个颜色算子(逐像素 RGB→RGB) ├── recipe.py # YAML 配方加载与 step 注册表 ├── lut.py # 33³ 恒等 LUT、向量化变换、.cube 写出 ├── manifest.py # manifest 条目构建 + JSON Schema 校验 ├── manifest_schema.json ├── deploy.sh # 上传 out/ 到 R2(wrangler) ├── requirements.txt ├── recipes/ │ └── cinematic/ │ └── teal_orange.yaml # 首个英雄配方(Teal & Orange) ├── tests/ │ ├── fixtures/sample.yaml + sample.cube (golden) │ ├── test_generate.py / test_lut.py / test_recipe_loader.py / test_transforms.py └── out/ # 生成产物(.gitignored) ├── cube/ # 60 × .cube 文件 └── manifest.jsonRecipe 格式(每个滤镜一个文件)
设计文档给出的配方示例如下,仓库中的真实文件 scripts/filters/recipes/cinematic/teal_orange.yaml 与之一致:
id: cinematic.teal_orange name: Teal & Orange category: cinematic accent: "#38BDF8" sort: 10 steps: - temperature: -8 - tint: +3 - contrast: curve: s_curve amount: 1.15 - split_tone: shadows: "#1E3A5F" highlights: "#FFA94D" balance: 0.0 - saturation: 1.10 - hue_shift: reds: -5每个字段的含义:
| 字段 | 说明 |
|---|---|
id | 全局唯一标识,如cinematic.teal_orange,同时作为.cube文件名与 manifest 索引键 |
name | 面向用户的显示名(如 "Teal & Orange") |
category | 所属分类(cinematic/portrait/vlog/retro/mood/bw) |
accent | 选择器中选中状态的主题色(十六进制),如#38BDF8 |
sort | 分类内排序权重,数值越小越靠前 |
steps | 按序执行的调色步骤列表,每一步都必须是逐像素 RGB→RGB 变换,才能折叠进 3D LUT |
v1 支持的 steps 全集
temperature、tint、exposure、contrast(linear/gamma/s_curve三种曲线)、saturation、vibrance、hue_shift(按通道或全局)、split_tone(阴影 / 高光 / balance)、lift_gamma_gain、channel_mixer(3×3 RGB 矩阵)、tone_curve(控制点)、clip(黑白电平)、monochrome(带通道权重)。
v1 明确排除:vignette、grain、blur、light_leak—— 它们不是 LUT 可表达的,推迟到程序化特效子系统。
源码中 scripts/filters/recipe.py 的STEP_REGISTRY把 YAML step 名映射到 scripts/filters/transforms.py 中的具体函数,例如temperature→apply_temperature(amount/100折算为 RGB 偏移)、exposure→apply_exposure(image * 2^stops)、contrast的s_curve用tanh实现软 S 曲线、saturation按 Rec.709 亮度系数[0.2126, 0.7152, 0.0722]加权。未知 step 会在加载时直接报错(对应测试 test_recipe_loader.py 的test_load_recipe_rejects_unknown_step)。
生成器算法
设计文档定义的四步算法,在仓库实现中逐一对应:
- 构建 33³ 恒等 LUT—— scripts/filters/lut.py 的
identity_lut()用np.linspace(0, 1, 33)在 R/G/B 三个轴上做 meshgrid 生成 33×33×33×3 的 float32 数组,LUT_SIZE = 33与设计一致; - 对 LUT 采样点向量化应用每个 step——
apply_transforms_to_lut()把 LUT 重塑为(-1, 1, 3)后逐函数作用,设计文档估计每张 LUT 约 5 ms; - 以 Adobe 标准格式写出
.cube——write_cube()依次输出TITLE、DOMAIN_MIN 0 0 0、DOMAIN_MAX 1 1 1、LUT_3D_SIZE 33及按 B→G→R 嵌套序排列的r g b三通道浮点值(%.6f),兼容 iOS 与 Android 两个既有解析器; - 把元信息追加进 manifest—— scripts/filters/manifest.py 的
build_manifest_entry()计算.cube的sha256与字节数,连同id/name/category/accent/sort/cubeUrl构成条目。
分类目录(v1:6 × ~10)
| Category | Filters(代表性示例,最终清单在 Phase 2 锁定) |
|---|---|
| Cinematic | Teal & Orange, Blockbuster, Movie, Noir, Hollywood, Drama, Bleach Bypass |
| Portrait | Soft, Warm, Golden, Porcelain, Natural |
| Vlog | Crisp, Vibrant, Punchy, Soft Pop |
| Retro | 70s, 80s, Polaroid, VHS, Sepia, Faded Film, Old Photo |
| Mood | Dreamy, Moody, Golden Hour, Cold Blue, Stormy, Soft Mist |
| B&W | Classic, High Contrast, Matte, Faded, Soft Mono, Gritty |
构建与部署
本地生成(scripts/filters/README.md 给出标准流程):
python3 -m venv .venv && source .venv/bin/activate pip install -r requirements.txt python generate.py # 输出到 out/cube/*.cube 与 out/manifest.json pytest tests/ -v # 运行工具层测试generate.py支持--recipes、--out、--base-url、--version参数,并按 6 分类的有序字典生成categories段(见 generate.py)。部署由 scripts/filters/deploy.sh 完成,对.cube使用public, max-age=31536000, immutable缓存头,对manifest.json使用较短的public, max-age=300, s-maxage=3600以便客户端快速发现新滤镜:
./deploy.sh # 等价于 aws s3 sync out/ s3://openreel-filters/ 的 wrangler 版本Hosting + Delivery:R2 公共桶与无 Worker 静态交付
R2 桶结构
独立桶openreel-filters(与openreel-templates、openreel-shares分离,便于生命周期与权限管理):
openreel-filters/ ├── manifest.json └── cube/ ├── cinematic.teal_orange.cube ├── cinematic.blockbuster.cube └── ... (60 files)Manifest 结构
{ "version": "2026-05-22T1", "minClientVersion": "1.0.0", "filters": [ { "id": "cinematic.teal_orange", "name": "Teal & Orange", "category": "cinematic", "accent": "#38BDF8", "sort": 10, "cubeUrl": "https://filters.openreel.video/cube/cinematic.teal_orange.cube", "sha256": "abc123...", "bytes": 154832, "oldIds": [] } ], "categories": [ { "id": "cinematic", "name": "Cinematic", "sort": 1 }, { "id": "portrait", "name": "Portrait", "sort": 2 }, { "id": "vlog", "name": "Vlog", "sort": 3 }, { "id": "retro", "name": "Retro", "sort": 4 }, { "id": "mood", "name": "Mood", "sort": 5 }, { "id": "bw", "name": "B&W", "sort": 6 } ] }关键设计点:
- 每个滤镜的
sha256与bytes支持下载后完整性校验与跨 manifest 版本复用决策(仓库中manifest.py已实现 sha256 计算,test_recipe_loader.py 的test_build_manifest_entry_includes_sha_and_bytes对此有断言); oldIds数组允许客户端在滤镜改名时透明重映射,避免用户片段悬空;- manifest 在写出前会通过 scripts/filters/manifest.py 用 manifest_schema.json 做
jsonschema.validate,CI 中亦有 schema 校验。
公共交付与缓存策略
GET https://filters.openreel.video/manifest.json → Cache-Control: public, max-age=300, s-maxage=3600 GET https://filters.openreel.video/cube/<id>.cube → Cache-Control: public, max-age=31536000, immutableR2 原生提供 ETag 与 304 语义,上传时通过wrangler r2 object put --cache-control设置缓存头;桶上一次性配置 CORS(PUT/GET/HEAD、*源),Web 编辑器可直接跨域拉取。manifest 中的cubeUrl使用绝对 URL,未来即使回迁 Worker 或更换宿主也不需要客户端发版。
版本化与缓存失效:
version字段采用 ISO 时间戳 + 计数器(如2026-05-22T1);- 客户端保存上次见到的 version,在启动与选择器打开时以
If-None-Match重取 manifest:304 → 用缓存,200 → 执行 reconcile; - LUT URL 按 id 稳定不变,缓存层校验
sha256,配方一旦变化自动使下游缓存失效。
客户端预取策略:
- 应用启动:后台低优先级拉取 manifest;
- 选择器打开:先下载可见区域的 LUT(约 10 个),其余后台续拉;
- Wi-Fi:预取全部 60 个(约 9 MB);
- 蜂窝网络:仅按需加载,可在设置中配置;
- LRU 磁盘缓存,上限 50 MB。
移动端数据模型、缓存与渲染集成
片段数据模型
data class AppliedFilter( val id: String, // matches manifest.filters[].id val intensity: Float, // 0.0 .. 1.0 ) // clip.filter: AppliedFilter? = null存储在与现有effects: []相邻的位置。存量项目反序列化时filter = null,行为与今天完全一致。
四个类,双平台相同表面
FilterCatalogService singleton, reactive state: StateFlow<FilterCatalog> ← .loading | .ready(filters, categories) | .error refresh() async ← fetch manifest, reconcile against on-disk snapshot Persists last good manifest so the picker is never empty after first successful launch. FilterLutCache fun get(id: String): LutData? ← memory hit suspend fun fetch(id: String): LutData ← disk hit → network fetch → sha verify fun prefetch(ids: List<String>) ← background, queued, cancellable LRU 50MB on disk. Parsed LutData (33³ float array) memoized in memory. FilterRenderer fun apply(image, filterId, intensity): image Looks up LUT via FilterLutCache.get (synchronous, must already be cached). If not cached: returns source unmodified and signals caller to await fetch. Mix: out = lerp(srcPixel, lutSample(srcPixel), intensity). FilterPickerViewModel state: StateFlow<{ categories, filters, selectedId, intensity, thumbnails }> onSelect(id), onIntensityChange(value), onCategoryChange(catId)边界收得很紧:渲染器不知道 HTTP,目录服务不知道 Metal/GL,缓存不知道 UI。
iOS 集成(插槽于Core/Rendering/VideoEffectRenderer.swift)
FilterCatalogService是actor,UI 通过@MainActor快照包装器绑定;FilterLutCache.fetch使用URLSession.downloadTask,文件落到Caches/openreel-filters/{id}.cube;- LUT 变成名为
CIColorCube的CIFilter(inputCubeDimension: 33、inputCubeData: Data); - 强度用
CIBlendWithMask:源 CIImage 与 LUT 应用后的 CIImage 之间,用一张 alpha = intensity 的纯色蒙版混合,单个 CIFilter、GPU 融合执行。
Android 集成(插槽于core/effects/ClipEffectPipeline.kt)
FilterCatalogService暴露StateFlow<FilterCatalog>,缓存使用 OkHttp +withContext(Dispatchers.IO);- 渲染器添加一个 Media3
GlEffect:把 33³ LUT 上传为GL_TEXTURE_3D,在 fragment shader 中逐像素采样;intensity 作为 uniform,GLSL 中执行mix(src, lut, intensity)。
片段渲染链顺序(双平台锁定)
source → LUT (filter @ intensity) → user color adjustments → spatial effects → outputLUT 排在最前,意味着用户后续的颜色调整可预测地叠加在滤镜之上(这是 CapCut 的行为);顺序颠倒会让调整在不同滤镜间表现不一致。
Filter Picker UX:CapCut 风格选择器
入口:选中视频片段时,上下文工具栏上现有的 "Filter" 入口保持不变,仅替换内部面板。
布局(双平台一致):顶部为实时预览区,其下是强度滑杆(带百分比、Reset/Apply按钮),再往下是分类 Tab(Recent / Cinematic / Portrait / Vlog / Retro / Mood / B&W),底部为横向滚动的滤镜贴图行,None始终是最左侧贴图。
交互细节:
- 点选滤镜 → 立即以 100% 强度应用,选中贴图显示对勾 + 按 manifest
accent着色的圆环; - 再次点选同一滤镜 → 关闭(回到 None);
- 拖动强度 → 实时预览,松手前不提交;选中 None 时隐藏滑杆;
Reset→ 强度回到 100%,滤镜保持选中;Apply→ 提交并关闭;不点 Apply 直接关闭同样会提交(CapCut 行为,用户有 Undo);- Recents Tab:记录跨项目的最近 12 个使用项,持久化在 user defaults / DataStore;
- 长按贴图 → "应用到全部片段",作为单一可撤销动作;
- 无障碍:VoiceOver / TalkBack 播报
"<滤镜名>, <分类>, <选中 | 未选中>";强度滑杆步进 5%;选中态用边框 + 对勾而非仅靠颜色区分(色盲安全)。
缩略图渲染管线
- 选择器打开 → 以低分辨率抓取当前预览帧(竖屏 144×256、横屏 256×144);iOS 从
MetalVideoView.lastFrame.pixelBuffer取,Android 从最新的 ExoPlayer surface texture 取; - 快照在内存中是单一 CIImage / Bitmap;
- 对每个可见贴图(初始约 8 个),在后台队列运行
FilterRenderer.apply(snapshot, filterId, 1.0); - 用户滚动时入队新贴图、取消视口外任务;
- 以
(filterID, snapshotHash)为键缓存[filterID → thumbnail],播放头移动超过 1 s 时失效。
单贴图状态机:
unknown → pending-lut → ready → rendered \─────────────▶ failed (retries on tap)pending-lut与failed贴图渲染占位符(accent 色 + 滤镜名),仍可点按,点按会提升下载优先级。
空态 / 失败 / 离线副本:
- 从未加载到 manifest:空态 + 重试按钮;
- 离线且无任何缓存:同上;
- 离线但有部分缓存:显示已缓存滤镜 + 横幅提示 "More filters available when you're online"。
错误处理与边界情况
- 项目引用了 manifest 中已不存在的滤镜:若 LUT 仍在本地缓存,片段正常渲染;选择器在 "Unavailable" 区以灰色显示该滤镜、禁止新应用;若 manifest 与缓存均无,则以
intensity = 0渲染并在检查器提示一行警告; - Schema 漂移:manifest 解码器忽略未知字段;新增必填字段由
minClientVersion门控,低于该版本的客户端隐藏受影响滤镜并提示升级; - 磁盘上 LUT 损坏:读取时校验
sha256,不匹配则删除 + 重取一次,二次不匹配贴图进入failed; - 磁盘满:
ENOSPC时缓存驱逐最旧的 5 条并重试;仍失败则仅内存保留 LUT 供当前会话使用,并弹一次性 "Storage full" 提示; - 下载中途应用进入后台:可断点续传(iOS 用 URLSession background 配置,Android 用 OkHttp
Range:);恢复后选择器随下载完成刷新贴图; - 两个片段并发打开选择器:目录与缓存均为单例,同一
filterId返回同一份 memoizedLutData,不会重复解析或下载; - 旧
filterPreset效果迁移:generate.py首次运行会把现有 20 个参数预设烘焙成.cube;项目加载迁移在AppState中把effects: [filterPreset(...)]重写为clip.filter: { id, intensity };旧渲染路径保留一个应用版本作为回退,随后移除; - 两个片段使用同一滤镜:共享同一份内存中的
LutData; - 滤镜 id 改名:manifest 的
oldIds数组在 reconcile 时透明重映射; - manifest 服务端 5xx:使用磁盘上的 last-known-good manifest,首次成功启动后选择器永远不会为空;
- 检查时点 vs 渲染时点:长导出开始时拍摄
LutData快照,导出中途 manifest 变化不影响本次导出; - 隐私 / 遥测:滤镜请求不携带 PII、不做按用户标记;未来可选聚合 "filter applied" 事件,同样无 PII。
测试策略与性能预算
工具层(scripts/filters/)
- 每个变换算子有单测,已知 RGB 三元组上容差 ±1 LSB(仓库中 test_transforms.py 覆盖
temperature、tint、contrast、saturation等核心算子); - Golden-file 测试:fixture 配方 → 期望
.cube,重新生成不一致即报 diff(test_generate.py 的test_sample_generates_expected_cube); - manifest 在 CI 中按 JSON Schema 校验(test_recipe_loader.py 的
test_write_manifest_validates_against_schema); - 每个配方冒烟测试:可加载、可生成、可确定性渲染固定色卡 PNG,提交的期望 PNG 作为视觉回归面。
托管层(R2 公共桶)
部署后 curl 冒烟:curl -sI https://filters.openreel.video/manifest.json应返回 200 + ETag + 正确Cache-Control;携带If-None-Match: <etag>重复请求应得 304;对.cubeURL 同样验证immutable头。
移动端服务层(无 UI)
FilterCatalogService用 fixture manifest 做 reconcile 测试:首次拉取、无变化重取、增/删/改/改名、未知字段容忍;FilterLutCache:内存 + 磁盘命中、未命中 → 拉取 → sha 校验、不匹配重试后失败、上限驱逐、并发拉取返回共享结果;FilterRenderer:intensity 0/50/100 的 golden-image 快照测试,容差 ≤ 1 LSB / 通道。
跨平台一致性(Phase 1 起为 CI 门禁)
同一组(sample.png, filterId, intensity)三元组分别通过 iOS 与 Android 渲染器,输出 PNG 逐通道比对,任一处偏差 > 1 LSB 即 CI 失败。
性能预算
- 选择器打开 → 首块贴图绘制:≤ 250 ms(中端设备、暖缓存),CI 强制;
- LUT 解析 + GPU 上传:≤ 20 ms / 滤镜,profiled 而非门禁;
- 1080p 每帧 LUT 通道:≤ 1 ms,profiled 而非门禁。
明确排除:端到端模拟器测试(不稳定、慢)、网络混沌模糊测试(失败面已被 mock 单测覆盖)。
实施阶段与风险登记
Rollout Phases
- Phase 0 — 基础(约 1.5 天,无用户可见变化):创建 R2 桶
openreel-filters+ 自定义域filters.openreel.video;搭建scripts/filters/(generate.py、transforms、schema、golden fixtures、1 个英雄配方 Teal & Orange);用wrangler r2 object put --cache-control部署并 curl 验证;CI 接入工具单测。退出门禁:manifest 与 Teal & Orange.cube在filters.openreel.video上以正确 ETag + Cache-Control 上线; - Phase 1 — 单滤镜端到端(约 4 天):Teal & Orange 贯通双平台移动管线;iOS 四类接入
VideoEffectRenderer、选择器替换旧滤镜面板;Android 同样四类接入ClipEffectPipeline;选择器显示 1 个滤镜 + None,强度可用,Apply 提交,跨平台一致性测试通过。退出门禁:TestFlight + Android 内部轨道对真实视频应用滤镜且肉眼一致; - Phase 2 — 目录规模化(约 5 天,可并行):编写剩余约 55 个配方;把现有 20 个
FilterPresetCatalog参数预设烘焙进 recipes/LUT;60 个全部过generate.py并部署 R2;60 个全部跑跨平台一致性;每个分类对真实素材做内部 QA。退出门禁:60 款滤镜上线、一致性全绿、QA 签字; - Phase 3 — 迁移与发布(约 2 天):
AppState项目加载迁移;旧VideoEffectType.filterPreset渲染路径保留一个版本后移除;提交 App Store + Play Store。退出门禁:带新选择器的公开版本; - Phase 4 — 内容迭代(每次零工程投入):新滤镜 = 写配方 →
generate.py→ 同步 R2,客户端下次启动自动生效;可选同一观感的双 manifest 条目 A/B;可选无 PII 的聚合分析。
风险登记
- 配方质量(最大风险):代码生成的滤镜不经肉眼调校容易"数学味过重"。缓解:Phase 2 对每个分类的真实素材做 QA;快速迭代闭环"改 YAML →
generate.py→ 上传 → 客户端即时生效"; - 跨平台颜色漂移:
CIColorCube与 GLES 3D 纹理采样之间的差异。缓解:Phase 1 起一致性测试作为 CI 门禁; - R2 成本 / 热路径:60 个 LUT × 多设备并非真实规模问题,但
Cache-Control: immutable是必须的,确保流量命中边缘缓存而非回源桶。
工作量估算
单人端到端约 2 周;两人(各自负责一个平台,Phase 0 后并行)约 1 周;另加 Phase 2 中 1–2 天的调色品味决策时间。
小结
Filter Presets v1 是一个把"调色能力"产品化的完整范本:以YAML 配方 + NumPy 向量化生成 33³ LUT的方式把滤镜从代码中的参数预设抽离为可版本化、可热更新、可跨平台一致的静态资产,借助R2 公共桶 + 不可变缓存做到"无需发版即可上新滤镜",并通过单例目录服务 + LRU 缓存 + sha256 完整性校验 + 四类组件分离在移动端保证首屏 250 ms 以内的体验与渲染一致性。仓库中 scripts/filters/ 目录已经落地了配方加载、变换算子、LUT 写出、manifest 校验与 R2 部署脚本等核心实现,配合 docs/superpowers/plans/2026-05-22-filter-presets.md 的实施计划,可作为后续开发或复刻此类"云端驱动滤镜目录"架构的直接参考。
【免费下载链接】openreel-videoOpenReel Video - Professional browser-based video editor. Open source CapCut alternative. 100% browser-based, no installation, no cloud uploads, no watermarks.项目地址: https://gitcode.com/GitHub_Trending/op/openreel-video
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考