Imported from Quriosity-agent/qcut (
qcut/.agents/skills/qcut-toolkit/jianying-video-effect-reference/SKILL.md). Install upstream withnpx skills add Quriosity-agent/qcut --skill jianying-video-effect-reference. Copyright stays with the author.
Jianying Video Effect (画面特效) Reference
Treat a Jianying 特效 as a four-layer relationship:
catalog card (effects2 / face-prop panel, http_cache JSON)
-> materials.video_effects[] material (draft)
-> own segment on a dedicated type:"effect" track
-> Cache/effect/<effect_id>/<md5>/ AmazingFeature package
-> single-input, seek-mode, event-parameterized renderer
This complements jianying-reference (generic package harvesting + stepped-frame capture — reuse its mtime-marker protocol and capture traps verbatim) and parallels jianying-transition-reference (transitions are the dual-input case; 特效 are single-input).
Layer 1 — Catalog (ressdk_db http_cache)
The 特效 panel is TWO API panels: effects2 = 画面特效 (effect_type: 7)
and face-prop = 人物特效 (effect_type: 8). All catalog state lives in
the http_cache table (raw response JSON keyed by hashed request URL); every
structured table (effect, category_effect, panel_*, loki_*) is empty
in current builds — do not query them.
~/Movies/JianyingPro/User Data/Cache/ressdk_db/rp_master.db # ressdk_db_info: hash_path -> (did, uid)
~/Movies/JianyingPro/User Data/Cache/ressdk_db/<hash>/rp.db # one per (device, uid); uid=0 = logged out
http_cache(id, url, response_body, version, timestamp) — two URL families:
/artist/v1/panel/get_panel_info_<32HEX>__jianyingpro_{0|beta}_…— panel category list + page 1 of the default category embedded indata.category_resources. The 32-hex token is a hash of the full request params, so the panel name is NOT in the URL; find the effects2 panel by matchingdata.categories[].category_idagainst itemcategory_ids./artist/v1/effect/get_resources_by_category_id_<32HEX>_<panel>_…— one 50-item page. Panel name IS readable in this URL suffix (effects2,face-prop,filter,transitions, …).data.next_offsetadvances 50→100→…,data.request_idstays constant per browsing session,has_more:falsemarks the last page.
effects2 categories observed (2026-08, 28 tabs, category_id 中文名 key):
39654 热门 rm · 7728 基础 basis · 7730 动感 cool · 7729 氛围 dream · 43957 最新 new ·
38510 潮酷 chaoku · 7735 边框 zoomout · 5914834 多屏 · 5914631 有声 · 39547 光 light ·
21924 爱心 heart · 5914352 音频 · 5913855 创意AI · 5913856 运镜 · 7734 自然 reality ·
39241 金粉 jinfen · 7731 复古 retro · 39246 电影 dianying · 15502 Bling kira ·
39539 扭曲 niuqu · 27966 综艺 zongyi · 7733 分屏 split · 5914473 宠物 · 5913770 投影 ·
5913775 纹理 texture · 7732 漫画 comic · 37381 暗黑 halloween · 39264 DV dv.
Beware: face-prop has its OWN 热门 (38389) — a draft category_id tells you
which panel the card came from.
Per-item fields that matter (data.effect_item_list[].common_attr unless noted):
| Field | Meaning |
|---|---|
effect_id / id |
artist-store id. Equals the Cache/effect/<dir>/ name only for recent packages; older ones sit under a legacy SHORT id (13661053, 2724384, …) that no current catalog row contains — the reliable catalog→disk join is md5 = inner dir name |
third_resource_id_str |
equals the DRAFT material's resource_id (e.g. 发光分身 7233250530292666939) |
title |
display name (开幕, 抖动, 发光分身, …) |
md5 |
package checksum = inner dir name on disk (the join key) |
item_urls[0] |
the ONE signed zip download URL (expires ~+1 yr) |
effect_type |
7 = 画面特效; 8 = 人物特效 EXCEPT the 写真 AI-portrait cards (category 5913867), which are 47 — never filter face-prop by type 8 alone |
sdk_extra (JSON string) |
setting.effect_adjust_params[] {effect_key, default, min, max} — the sliders |
extra (JSON string) |
effect_duration (ms, almost always 3000), is_vip, sliders (effect_key → Chinese slider label) |
requirements[] |
renderer/CV capabilities: blit, matting, face, script, depth, … |
model_names / sdk_model |
AI model deps (tt_matting, tt_face, tt_face_extra) |
business_info.json_str |
is_vip + paid_type (~68% of effects2 are VIP/subscribe) |
special_effect.effect_duration |
default duration in ms, duplicated in extra.effect_duration with identical values (special_effect is the more complete copy — it keeps 0 where extra omits the key) |
Top slider keys by frequency: effects_adjust_speed (545/680), intensity,
luminance, blur, background_animation, filter, size, color,
range, distortion, horizontal_shift, vertical_shift,
horizontal_chromatic, sharpen, … — normalized 0–1 in catalog and draft
(~99.5%; a handful declare real ranges, e.g. luminance max 2.3).
Coverage caveats: only categories the user actually browsed have cached pages
(one row per 50-item scroll page); packages download lazily on FIRST APPLY, so
a machine can know ~1000 catalog effects yet hold only ~10 on disk (2026-08-16
measurement: 1024 rows — 707 effects2 + 317 face-prop — of which 618 are
blit-only). To widen the CATALOG, browse tabs in the app; but packages
themselves no longer need in-app clicking — item_urls[0] is directly
fetchable and md5 verifies the zip (see the batch pipeline section below).
Layer 2 — Draft (materials.video_effects + effect tracks)
Current draft_info.json is encrypted; evidence comes from plaintext
*/.backup/*.load.bak files (rare — scan with head -c5 | grep '{').
An applied 特效 = one materials.video_effects[] entry + one segment on a
dedicated type:"effect" track. The segment's material_id is the
material UUID and its extra_material_refs is []. Video segments NEVER
reference video_effects (contrast materials.effects — filters/adjusts — which
can be clip-attached via extra_material_refs). Material schema:
{
"id": "<UUID>", // what segment.material_id points to
"type": "face_effect", // observed for face-prop; 画面特效 presumably "video_effect" (unverified)
"name": "发光分身",
"effect_id": "13661053", // Cache/effect/<effect_id>/ dir (legacy short id — NOT the catalog effect_id)
"resource_id": "7233250530292666939", // = catalog third_resource_id_str; ≠ effect_id (filters have them equal)
"path": "…/Cache/effect/13661053/dbff6732319cf9488da4816c188d9f1a", // container path symlinks to ~/Movies
"category_id": "38389", "category_name": "热门", // which panel tab it came from
"adjust_params": [{"name": "effects_adjust_intensity", "default_value": 0.8, "value": 0.8}],
"apply_target_type": 2, // only value observed; effects[] filters use 0.
// Community convention 0=clip 2=global — NOT verified locally.
"apply_time_range": null, "time_range": null, // timing lives on the segment
"value": 1.0, // overall strength
"algorithm_artifact_path": "##_draftpath_placeholder_<GUID>_##/video_effect/multi_faces_algorithm/<UUID>",
"disable_effect_faces": [], "common_keyframes": []
}
Effect segment specifics: universal segment schema with
source_timerange: null, clip: null (generated content, no media/transform),
target_timerange in microseconds, default duration: 3000000 (3 s,
matching the catalog's effect_duration), start = playhead at apply time.
Render-order bands (render_index_track_mode_on: true): main video 0, PIP 2,
filter tracks 10000+, effect tracks 11000+ (one +1 per effect segment in
creation order), sticker 14000+. So 特效 composite above filters and below
stickers. track_render_index is just the track's array index.
adjust_params[].default_value mirrors the package extra.json defaults
exactly; value is the user's slider state.
Layer 3 — Package (Cache/effect AmazingFeature bundles)
Cache/effect/<effect_id>/<md5>/ — shared by transitions/masks/beauty too
(filters live in Cache/artistEffect/ instead). Two md5 dirs can coexist under
one effect_id (old + updated package versions — a draft's path may pin the
older md5 while the catalog already lists the newer one); a sibling
<md5>_modity_time.txt (present on the most recently downloaded version)
lists the extracted file manifest.
Every 特效 package is an Amazing-engine bundle (NOT the .lsproj node
graph — in this cache that format belongs exclusively to text animations):
config.json # effect.Link[] {path, type:"AmazingFeature", zorder≈8011}; bALG_BACH_CONFIG
extra.json # setting.effect_adjust_params — same schema as catalog sdk_extra
algorithmConfig.json # optional CV graph: nodes[] {type: blit|face|matting, config}, links[]
AmazingFeature/
main.scene # binary %SerializedFormat% scene
sticker.config # engine systemList + dev/min_version gate
material/ rt/ mesh/ # entity-material graph, render targets
xshader/*.frag # PLAINTEXT GLSL, and/or compiled plaintext pairs under
Library/ShaderData/<hash>/shaderGLES|shaderMetal/
lua/SeekModeScript.lua, ImageBusinessSlider.lua, …
Renderer families observed (classify before porting):
| Family | Signature | Examples |
|---|---|---|
| (a) plain shader pass(es) | 1–3 xshader passes + SeekModeScript, no algorithm | 抖动, 卷动, 泡泡变焦 |
| (b) scene node graph | multi-entity multi-RT material chain | 发光分身 (matte→ghost splits→8-octave gaussian glow→blend), 电光眼 (face mesh + PNG Aniseq) |
| (c) AE/Lumi export | AE2Effect/AEExporter config, lua/LumiFamily/*, per-node effects/ dirs |
竖线屏闪, 云雾消散 |
| (d) baked media | per-aspect PNG seq/ + .seq/.imageatlas, no math |
怀旧边框 II, 胶片框 |
| (e) algorithm hybrids | multiple AmazingFeature links (z 8011/8012/8013), face+matting models | 撕纸特写 |
Render contract (the part QCut parity work must honor):
- Single input texture — shaders sample one
inputImageTexture; Lua readsAmaz.BuiltinObject:getInputTextureWidth/Height(); Lumi scripts getInputTex/OutputTex/PingPongTex. Extra samplers are internal RTs or CV masks (matting masks arrive y-flipped, value in.r), never a second clip. - Seek-mode time — families (a)(b)(d)(e) carry a
SeekModeScript.lua; progress =(curTime - startTime) / (endTime - startTime)withendTime = 3.0in most packages (a few use 10.0/0). The host-write mechanism varies: some scripts declare an explicit--@input float curTimeslider, others are driven through an autoplay/playTime field. Family (c) Lumi/AE packages have NO SeekModeScript —lua/LumiFamily/LumiManager.luaaccumulatesdeltaTimeinonUpdate(default endTime 6.0) and pushes start/end/curTime down the layer tree, so they are wall-clock-stepped, not closed-form scrub-safe. Verify which contract a package uses before assuming QCut-stylef(progress)parity. - Params as events — the host dispatches
onEvent(key = "effects_adjust_*", value ∈ [0,1])with the draft's normalized value. An auto-generatedImageBusinessSlider.lua(headerwrite by editor EffectSDK) remaps to real uniform ranges declared data-side inAmazingFeature/ImageBusinessSlider.json(per-key →{entity, material, uniform, minValue, maxValue}; e.g. 发光分身 intensity→u_GlowIntensity@Blend [0,1], size→u_Scale.y@displace0 [0.05,0.5]). So draft 0–1 values are meaningless without the package's remap table — harvest it before calibrating. - CV pipeline —
requirements+model_names(catalog) declare it;algorithmConfig.jsonis the executable graph (blit downsample size, face detect ability flags). Effects needing matting/face cannot be ported as pure shaders; QCut needs its own segmentation/landmark source or a documented known difference.
Rendering a package through the local runtime (verified)
QCut can render these packages by driving the Jianying runtime installed on the machine — the same libraries the Transition Lab uses. Decoded by reading each created object's vtable pointer back to its symbol:
SwingSegmentType: 0 = FeatureSegment (特效), 2 Sticker, 3 Text,
4 Template, 5 Emoji, 6 Custom, 7 Video, 8 Transition, 9 StickerBrush,
10 Script. 1/11/12 are unmapped and crash — the factory bounds-checks <= 0xa.
Render contract (proven across every family above, including Lumi/AE):
bef_swing_manager_create_with_gpdeviceinside a GL/Metal context, thenset_parameter_bool(manager, "EnableSwingSimplify", true)— required, or nothing renders at all.- Video segment = type 7; effect = type 0 created WITH the package path (that
is what loads
main.scene). bef_swing_segment_video_add_feature(video, feature)— a 特效 is a feature ON the video segment, not a standalone segment. Adding it to the manager as well trips_preProcessWithoutTracks: invalid segments, two -1 layer foundand voids both segments.- Per frame:
video_set_device_texture(video, &input)thenmanager_seek_frame_device_texture(manager, timestampMicroseconds, &input, &output). - Sliders arrive as
effects_adjust_*key/value pairs (normalized 0–1, straight from the draft).
The trap that wastes hours: most effects are IDENTITY at most timestamps — 抖动 differs only near 0.2 / 0.6 / 1.2 s. A single-timestamp test reads as "the effect never rendered". Sweep time, or validate against an always-on overlay (胶片框 / 怀旧边框 II hold a constant ~17–19 mean channel difference).
The runtime needs the FULL 23-library closure
(~/Library/Application Support/qcut/PrivateRuntimes/JianyingTransition/current);
the 5-library .local/jianying-runtime root segfaults during manager init.
Implementation: research/jianying-runtime-probe/effect-probe.mm +
electron/jianying-effect/ (PR #414).
Batch reference pipeline (参照生产线)
scripts/jianying-effect-reference-batch.cjs mass-produces ground-truth
reference clips for every blit-only catalog effect — the scale path chosen
2026-08-16 (the lab itself cannot scale to end users: macOS ∩ JianYing
installed ∩ package cached; references drive QCut-native reimplementation
instead, like the filter LUT fitting and sound-effects lab precedents).
bun run build # dist/electron must be fresh — the script requires it
node scripts/jianying-effect-reference-batch.cjs # full run
node scripts/jianying-effect-reference-batch.cjs --limit 5 # smoke
node scripts/jianying-effect-reference-batch.cjs --panel effects2
node scripts/jianying-effect-reference-batch.cjs --only <effectId,...>
Mechanics worth knowing before touching it:
- node, never bun — the catalog reader statically needs
node:sqlite. Thedist/electron/jianying-effect/*modules have zero electron imports, so plain node canrequire()them (getFFmpegPathresolves the staged dev ffmpeg). - Acquisition: reuses packages already in JianYing's caches (md5-indexed),
else fetches
item_urls[0](UA-spoofed, 500 ms throttle), verifiesmd5(zip) == common_attr.md5 == dir name, unzips into.local/jianying-effect-references/_packages/<effectId>/<md5>/. JianYing's own cache is never written to —definition.packagePathaccepts any directory. - Render: 6 s / 1280x720 / 30 fps via
renderJianyingEffectClip, default slider values from the packageextra.json(fallback catalogsdk_extra), effect window spanning the whole clip. ~5-6 s per effect cached, 8-13 s with download. - Reference clip: real footage (skate) + SMPTE bars + grayscale ramp strips — fit on real colors only (LUT-fitting lesson).
- Manifest:
manifest.jsonlappend-only, one line per effect (md5/seconds/frames/ssim/adjust params/error); reruns skipok:truerows, so interrupted runs just resume. - SSIM ≠ identity check: sparse effects legitimately score ~0.99 (星火);
only >0.997 gets
flaggedIdentity, and even that needs a frame sweep before concluding (same trap as the single-timestamp one above). - The Lumi "failures" were a truncation artifact, not a runtime limit. A
2026-08-17 run reported 517/618 with all 101 failures in the Lumi family
(~90 the JS variant: root
LumiManager.js+config.json.js_path+ embedded ThreeJS; rest LuaLumiFamily/), and the family's known wall-clock/onUpdatecontract made "needs native stepping" look obvious. Running the bridge directly disproved it: exit 0, a complete raw output, the[effect] frames:receipt present in stdout, and 60/60 frames differing from the input. The receipt sat 10,268 bytes from EOF whilerender.tskept only the last 8,192 — Lumi's JS-engine teardown ([AE_JSRUNTIME_TAG]'Scene: 开始清理场景资源'plus dispose/destroy stacks, ~90KB) scrolled it out. Packages without a JS engine barely log at teardown, which is why the false failures landed exactly on one family. Fixed by latching the line as it streams (retainPattern); all 101 then passed, taking the library to 618/618. - Diagnostic rule this cost us: when the bridge "finishes without reporting", read the FULL raw stdout before theorising about the render contract. Tail-window truncation is a silent source of false failures. (A failed pass still muxes an mp4 — the third ffmpeg step runs before the receipt is parsed — so the batch script deletes its output on failure.)
- The lab UI consumes
manifest.jsonlas a verification ledger: effects with a failing verdict are locked as 「本机渲染验证未通过」 instead of pretending to work (electron/jianying-effect/catalog.ts). - Outputs live in
.local/jianying-effect-references/(gitignored). Same red line as everything else here: packages and rendered references never enter git, public storage, or the product. Team sharing goes through the sound-effects-lab private-bucket + allowlist pattern.
Companion doc: docs/task/jianying-effect-reference-line/README.md.
Harvest protocol
- Map card → package with the mtime-marker loop from
jianying-reference (apply ONE card, find
the new
Cache/effectdir; already-cached cards leave zero disk trace). - Read
extra.json+ImageBusinessSlider.jsonfirst (param schema + remap ranges), then classify the family (table above) before reading shaders. - Compiled shaders under
Library/ShaderData/are plaintext even when.auslsources are encrypted — same trick as text animations. - Capture stepped reference frames per jianying-reference's protocol (loop effects need the play-pause capture variant).
- All of jianying-reference's Capture Traps apply unchanged.
QCut landing zone (2026-08 state)
- Panel EXISTS:
apps/web/src/components/editor/media-panel/views/effects.tsx(tabeffects, label 特效, flagVIDEO_EFFECTSinconfig/features.ts), ~166 published presets inapps/web/src/lib/effects/effect-catalog.ts(+13 per-kind catalog files). - Render model:
EffectRenderProgramwith 9 stage kinds inpackages/editor-core/src/types/effect-render.ts; stage types are DUPLICATED inelectron/ffmpeg/effect-render-types.ts— mirror both when adding a kind. Preview seam:preview-panel/use-effects-rendering.ts+preview-element-renderer.tsx. Export seams: canvas (lib/export/export-engine-renderer.ts) and CLI/FFmpeg (lib/export-cli/sources/effect-*-sources.ts). - Timeline: active mechanism is per-clip
element.effects: EffectInstance[](synced withstores/ai/effects-store.ts);TrackType "effect"andEffectElementtypes exist in editor-core but nothing constructs them — Jianying's independent draggable effect clips are a gap. - CapCut draft export does NOT emit
materials.video_effects(unsupported-features.tsraises an error for element effects); import maps the bucket to segment kindeffectwith capabilityopaque. Both are open parity work. - Prior research to reuse:
docs/task/effects-pack/(JIANYING-LIST.md, MAPPING.md, PRIMITIVES.md, CHECKLIST.md) — panel screenshots, 44 mapped gaps, stage capability boundaries, add-one-effect recipe.
Scope notes
- Read-only analysis of locally cached files for interop/parity. Do not
redistribute Jianying assets or ship harvested content — and that covers the
REPO, not just the product: no decompiled shaders, prefab dumps,
stringsoutput, or extracted media in commits. Transcribe behavior into your own equations/prose; raw files stay in the session scratch dir. - Numbers above (category ids, counts, band values) were measured on the
2026-08 CN build (draft
version 360000, app 5.9.x); re-verify ids after app updates — catalog hashes embed the app version, so stale rows linger beside fresh ones in http_cache.