13 次代码提交
作者 SHA1 备注 提交日期
dayu 3bb33818ea Release 1.1.0: record notarized installation, native card template and source recovery 2026-10-21 23:04:40 +08:00
dayu 80f22e2285 docs: record notarized 1.1.0 and independent source recovery before host exit 2026-10-21 22:57:38 +08:00
dayu f79d24eb01 Fix inspector undo, time changes and legacy disabled-state migration; verify secure template and 1039 regressions 2026-10-21 22:53:12 +08:00
dayu f4c0d608a4 Build native card inspector and secure state; hide manual analysis and unify automatic follow (validation in progress) 2026-10-21 22:45:37 +08:00
dayu 5793e3de49 Record installed signed 1.0.6, synchronized template and final delivery verification 2026-10-21 21:55:19 +08:00
dayu 9bebf68e3f Record orientation negative control, accepted 1.0.6 notarization and independent remote recovery while awaiting host exit 2026-10-21 21:00:33 +08:00
dayu ad54726b17 Fix FxPlug CI orientation and preserve incompatible legacy tracks; prepare tested 1.0.6 Release 2026-10-21 20:56:54 +08:00
dayu 16525d62e5 Align chin deformation with recovered three-pin signed mapping; add anatomical and numeric regressions 2026-10-21 20:49:38 +08:00
dayu 1008651f6e Record signed 1.0.5 installation, contour-slim regressions and user visual test handoff 2026-10-21 20:25:32 +08:00
dayu a6551c334f Resume verified notarized releases and make staging unregistration idempotent 2026-10-21 20:23:10 +08:00
dayu abe97ed0cb Unregister generated plug-in immediately after Release build before notarization 2026-10-21 20:19:48 +08:00
dayu ec22f1b2cf Align slimming with 76 indexed landmarks and 17 sequential local pins; pass 604 offline checks 2026-10-21 20:19:32 +08:00
dayu 2a856c669a Analyze contour-based reference slimming and profile deformation gap; preserve installed 1.0.4 2026-10-21 19:02:24 +08:00
共修改 79 个文件,包含 19086 行新增和 258 行删除

没有与搜索条件相匹配的文件

+24 -2
查看文件
@@ -2,11 +2,11 @@
基于本仓库的 FxPlug4 初始工程制作的人像美化滤镜,支持 Motion 和 Final Cut Pro。源码使用 Objective-C、Vision、Core Image 与 Metal。 基于本仓库的 FxPlug4 初始工程制作的人像美化滤镜,支持 Motion 和 Final Cut Pro。源码使用 Objective-C、Vision、Core Image 与 Metal。
功能包括磨皮、肤色、细节、牙齿美白、眼睛提亮、九类脸型调整、画面选脸、三目标分析跟踪扩展和蒙版预览。原版 MSE18V2 的静态分析依据、近似实现与尚未对齐的部分见 [分析说明](docs/analysis/REA-分析与复刻说明.md)。本版尚未完成原版逐像素与复杂运动跟踪对照。 功能包括磨皮、肤色、细节、牙齿美白、眼睛提亮、九类脸型调整、自动人脸跟随和多人处理。原版 MSE18V2 的静态分析依据、近似实现与尚未对齐的部分见 [分析说明](docs/analysis/REA-分析与复刻说明.md)。本版尚未完成原版逐像素与复杂运动跟踪对照。
## 使用 ## 使用
正式安装路径为 `/Library/Plug-Ins/FxPlug/beatygril.app`。Motion 滤镜分类为「大鱼」,滤镜名「大鱼人像美化」。Final Cut Pro 在「效果 → 大鱼 → 大鱼人像美化」中使用配套 Motion 效果,模板目录必须同时保留 `.moef` 与 `Drop Zone.tiff`。美化会随当前帧的人脸自动更新,不需要先点分析;默认脸型跟随主要人脸,开启多人高速模式后对多脸变形。可按需调中频磨皮、肤色与脸型;「只处理选中人脸」用于画面重选目标。三个目标的向前/向后分析属于可选扩展,数据可随工程保存。 正式安装路径为 `/Library/Plug-Ins/FxPlug/beatygril.app`。Motion 滤镜分类为「大鱼」,滤镜名「大鱼人像美化」。Final Cut Pro 在「效果 → 大鱼 → 大鱼人像美化」中使用配套 Motion 效果,模板目录必须同时保留 `.moef` 与 `Drop Zone.tiff`。美化会随当前帧的人脸自动更新,不需要点击分析。界面以磨皮、美白、瘦脸三张卡片呈现,默认跟随主要人脸;多人模式对多脸变形。单项眼睛可暂时停用并保留数值,右侧箭头恢复默认;磨皮半径、饱和度、锐化和整体混合等收在「更多设置」。旧手动分析与调试参数仅保留隐藏兼容,实际输出统一使用实时自动跟随。
## 开发和恢复 ## 开发和恢复
@@ -22,3 +22,25 @@ xcodebuild -project beatygril/beatygril.xcodeproj -scheme 'Wrapper Application'
工程从 `/Library/Developer/SDKs/FxPlug.sdk` 读取头文件与链接信息,从 `/Library/Developer/Frameworks` 嵌入可运行的 Apple 框架。构建默认使用团队 B3W25FX5H5 的 Developer ID Application 身份;正式交付使用 `python3 scripts/deliver.py` 完成公证、粘附和安装;安装前先关闭 Motion 与 FCP,脚本在检测到宿主仍运行时保留原安装版并停止。原生验收完成后继续脚本的清理阶段,清除临时注册与 APP 副本。 工程从 `/Library/Developer/SDKs/FxPlug.sdk` 读取头文件与链接信息,从 `/Library/Developer/Frameworks` 嵌入可运行的 Apple 框架。构建默认使用团队 B3W25FX5H5 的 Developer ID Application 身份;正式交付使用 `python3 scripts/deliver.py` 完成公证、粘附和安装;安装前先关闭 Motion 与 FCP,脚本在检测到宿主仍运行时保留原安装版并停止。原生验收完成后继续脚本的清理阶段,清除临时注册与 APP 副本。
私有源码仓库:`https://dayu.media/git/dayu/paintfcp-fxplug.git`。恢复入口见 [RECOVERY.md](RECOVERY.md),当前验证记录见 [验证记录](docs/验证记录.md)。原始模板保存在 `baseline-20261021` 标签。 私有源码仓库:`https://dayu.media/git/dayu/paintfcp-fxplug.git`。恢复入口见 [RECOVERY.md](RECOVERY.md),当前验证记录见 [验证记录](docs/验证记录.md)。原始模板保存在 `baseline-20261021` 标签。
## 1.0.5 瘦脸修复
瘦脸现固定使用Vision revision 3的76点配置,以17组轮廓到鼻部/瞳孔锚点的局部逆向采样替代脸框水平挤压,包含原版尺寸增益与滑块强度映射。完整点阵贯通实时稳定与离线存储;旧分析记录没有完整76点时跳过瘦脸,重新分析可补齐。其他脸型项仍使用原有近似实现,不能将本次对齐扩张为全部脸型逐像素等价。
离屏验证新增224项:含48组脸部投影/尺寸/歪头与独立17-pin数值场对照、数据恢复和缺点保护;完整回归604项通过。Motion/FCP界面由用户自测。可运行 `python3 scripts/deliver.py --wait-for-hosts --user-tests`,签名公证后只读等待宿主自然退出,安装验证与清理无需操作宿主界面。
## 1.0.6 下巴与图像坐标修复
下巴改为76点中68→38、67→37、66→36的三组局部位移,正值上收、负值延伸,保留原版正负不同系数及尺寸增益。Core Image/IOSurface输入输出移除按TOP_LEFT额外翻转,和原版图像链对齐。修复依据与离屏复现见[下巴与坐标分析](docs/analysis/下巴与坐标分析.md)。七类其余脸型项仍是近似实现。
旧版分析记录无法区分是否包含额外翻转,新版保留原字符串但暂用实时跟随;重新分析即可写入新坐标标记并恢复离线跟踪。最终Motion/FCP画面由用户自行测试。
1.0.6(8)已完成正式签名、Apple公证、票据、安装和模板同步。904项完整回归及独立远程恢复核验通过;最终宿主视觉由用户自测。交付回执在`docs/verification/v1.0.6/`,恢复标签为`v1.0.6`。
## 1.1.0 原生卡片面板
使用AppKit自绘卡片、滑块和开关,原生数值输入、肤色圆点、单项/分组启停及重置。FCP仅发布自定义面板210和两项版权信息,撤掉平铺参数与手动分析板块。新面板使用独立NSSecureCoding状态对象完整保存设置,支持复制、撤销数据和数值插值;未编辑时保持旧文档参数值。旧分析数据不参与实时渲染。
`./scripts/test-inspector.sh`验证参数路由、保存恢复、事务、绕过/重置和离屏布局;`python3 scripts/inspector-template.py audit Templates/大鱼人像美化/大鱼人像美化.moef`验证模板自定义数据和发布入口。离屏截图和模拟宿主检查不能替代Motion/FCP的实际嵌入、键盘/VoiceOver和工程重开验收,仍由用户自测。完整1039项回归通过,Inspector/宿主适配/OSC启用ASan。离屏演示图见[面板预览](docs/verification/v1.1.0/inspector-preview.png)。1.1.0(9)已完成通用Release正式签名、Apple公证、票据粘附、唯一正式目录安装及配套模板同步。签名源码`f79d24eb0185f31feaae58b83b632f92dcb3ca91`的111文件及16项必要资源已独立远端恢复核验,clone内134项Inspector ASan和模板审计通过。
正式安装回执见`docs/verification/v1.1.0/installed-verification.json`,恢复标签为`v1.1.0`。签名、票据、Gatekeeper、双架构身份、PluginKit正式路径及配套模板字节核验通过;已清理本次临时APP、旧APP备份和公证ZIP。有界扫描只发现正式路径的产品外层APP,未把该范围表述为所有卷的完整扫描。未操作Motion/FCP界面,实际面板嵌入与工程重开由用户自测。
+39 -1
查看文件
@@ -12,7 +12,7 @@
```sh ```sh
git clone https://dayu.media/git/dayu/paintfcp-fxplug.git git clone https://dayu.media/git/dayu/paintfcp-fxplug.git
cd paintfcp-fxplug cd paintfcp-fxplug
git switch --detach v1.0.4 git switch --detach v1.1.0
``` ```
需要继续编辑时,基于该提交创建新分支。安装 Xcode 与 Apple FxPlug 4 SDK;盘点版本和构建入口见 README.md。SDK/framework 为 Apple 外部开发依赖,应重新安装匹配版本。证书、私钥和公证凭据需从独立加密备份恢复或重新申请;Git 不包含它们。 需要继续编辑时,基于该提交创建新分支。安装 Xcode 与 Apple FxPlug 4 SDK;盘点版本和构建入口见 README.md。SDK/framework 为 Apple 外部开发依赖,应重新安装匹配版本。证书、私钥和公证凭据需从独立加密备份恢复或重新申请;Git 不包含它们。
@@ -36,3 +36,41 @@ git switch --detach v1.0.4
FCP模板从 `Templates/大鱼人像美化/` 恢复,将 `.moef` 和 `Drop Zone.tiff` 一起放入 `~/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/`;只放 `.moef` 会缺少标准效果源输入资源。正式APP仅安装在 `/Library/Plug-Ins/FxPlug/beatygril.app`,不是从工作树或构建目录运行。没有LFS或子模块,必要工程资源、NASA测试人像及配套模板资源均在Git中。 FCP模板从 `Templates/大鱼人像美化/` 恢复,将 `.moef` 和 `Drop Zone.tiff` 一起放入 `~/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/`;只放 `.moef` 会缺少标准效果源输入资源。正式APP仅安装在 `/Library/Plug-Ins/FxPlug/beatygril.app`,不是从工作树或构建目录运行。没有LFS或子模块,必要工程资源、NASA测试人像及配套模板资源均在Git中。
用户用于宿主验收的外置卷视频及build内的验收工程/截图属于测试素材与可重建产物,不属于产品构建依赖,也没有远程上传。原MSE18V2 APP与原版MS模板仅为只读参考,不作为运行依赖。远端恢复记录及实际宿主测试范围见 `docs/验证记录.md`。 用户用于宿主验收的外置卷视频及build内的验收工程/截图属于测试素材与可重建产物,不属于产品构建依赖,也没有远程上传。原MSE18V2 APP与原版MS模板仅为只读参考,不作为运行依赖。远端恢复记录及实际宿主测试范围见 `docs/验证记录.md`。
## 瘦脸与侧脸问题的分析节点
`v1.0.4`之后的分析提交包含`docs/analysis/脸型变形与转脸分析.md`、`slim-pose-report.json`、REA证据包及独立Vision离线检测工具`scripts/inspect-face-geometry.m`。产品源码与安装版没有因该分析改变;已查明的问题仍存在于1.0.4。复现检测工具的依赖与命令见分析文档,用户视频、完整人脸坐标和帧图保留本地,不是产品恢复依赖。
## 1.0.5 当前恢复入口
当前发布标签为 `v1.0.5`,APP版本1.0.5、build7。签名构建的完整源码SHA为 `abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964`;最终标签还包含交付脚本恢复修正及验证记录,产品源码与该SHA一致。该节点已从独立远端克隆恢复,69文件及9项必要资源完整、389项核心测试通过。主工作区另通过604项完整回归及同一用户视频11帧的264项瘦脸ASan场景。
从最终标签恢复后,关闭Motion/FCP,运行 `python3 scripts/deliver.py --wait-for-hosts --user-tests` 可完成签名公证安装、只读宿主退出等待、静态发现及副本清理;它不会操作宿主界面。若上次因非产品错误在Accepted且stapled后停止,可加 `--resume-prepared`,脚本必须重新核对签名CDHash、版本、票据及产品源码未变才会继续。缺少原staging或回执时应完整重新构建,不能伪造回执。
1.0.5瘦脸需要完整76点;旧离线记录缺该字段时保留其余功能并跳过瘦脸。关闭使用跟踪数据可使用当前帧自动76点,或重新分析生成新记录。没有引入原版metallib或CoreML模型,运行只依赖本产品及Apple系统框架。最终安装、配套模板和扫描范围见验证记录。
## 1.0.6 恢复入口
本次签名构建源码SHA为`ad54726b17fabd11f46bc6121e664eac38960a23`,APP版本1.0.6、build8;最终发布标签为`v1.0.6`。恢复该SHA可重建相同产品源码,最终标签另包含交付文档与回执。Release恢复仍用上节同一构建/公证命令、固定Bundle ID和正式安装路径;用户自测模式不会操作Motion/FCP界面。
新增下巴三pin与图像适配轴修复;新分析档案使用`hostCoordinateSpace=iosurface-ci-y-up-v1`。旧未标记档案保留原字符串但回退实时跟随,重新分析后才能用于离线重放,不能直接补字段伪装成新档案。七类剩余脸型项及组合顺序仍有近似边界。
`ad54726b17fabd11f46bc6121e664eac38960a23`已独立从上述远程克隆,87文件的byte/blob/mode与14项资源一致,strict/full fsck和559项核心测试通过,无LFS、子模块或APP产物。临时恢复目录已清理,完整报告见`docs/verification/v1.0.6/recovery-ad54726.json`。
1.0.6已完成正式签名、公证(Accepted ab0271c8-0c4b-4b10-9474-cebe9b55385b)、票据粘附、唯一正式目录安装和配套模板同步;临时APP/旧备份APP/公证ZIP已清理。完整回执及有界副本扫描见`docs/verification/v1.0.6/installed-verification.json`。未操作Motion/FCP界面,最终视觉验收保留为用户自测。
## 1.1.0 面板与自动处理
1.1.0(9)使用自有原生卡片面板和PGInspectorState安全归档。源代码、面板资源(全部程序绘制)、Motion模板与自有自定义参数数据均在仓库;新增`./scripts/test-inspector.sh`及`scripts/inspector-template.py audit`验证入口。恢复时需完整保留`.moef`中的自定义参数210、实际dataValue/defaultVal和两项版权发布;不要删掉仍用于旧工程迁移的隐藏参数定义和组路径。不得拿其他插件的自定义归档替换本产品自己的归档。
新渲染统一实时自动人脸跟随,旧手动分析数据仍保存在旧文档中但不参与本版渲染;主界面不再显示前后分析、目标列表、蒙版调试与平铺控件。三张卡片之外的细调收在更多设置。自定义对象保存完整界面状态,首次编辑前兼容旧参数;数值、开关、启停mask按已测试的复制/插值规则恢复。真实宿主UI及工程重开仍需用户自测。
正式构建、公证、安装仍使用同一`python3 scripts/deliver.py --wait-for-hosts --user-tests`,保持固定Bundle ID与APP路径,仅在用户自行退出Motion/FCP后替换,最终版本节点与回执随交付记录补齐。
兼容边界:未编辑面板时读取旧文档原参数;首次编辑会把当前数值采集为完整面板状态。旧版各项独立关键帧曲线尚未自动迁移为新面板关键帧,不宣称编辑前后任意旧动画逐帧等价。面板本身的数值插值与开关保持已做离屏验证,实际宿主关键帧交互仍待用户测试。
1.1.0签名构建源码为完整SHA`f79d24eb0185f31feaae58b83b632f92dcb3ca91`,源码工作区在构建时干净。该节点已独立从大鱼远程克隆,111文件的bytes/blob/mode及16项必要资源一致,strict/full fsck、134项Inspector ASan和模板审计通过,无LFS或子模块,临时克隆已清理。报告见`docs/verification/v1.1.0/recovery-f79d24e.json`。Apple公证Accepted(ce26be63-9dcd-436a-bcb5-bd294fcc92a9)和票据验证已通过;安装前曾等待用户自行退出宿主,随后完成下述正式替换。
1.1.0(9)现已安装到固定正式路径;APP与嵌套插件为arm64/x86_64通用版本,Developer ID团队、可信时间戳、Hardened Runtime、严格签名、公证票据、Gatekeeper及PluginKit正式路径全部核验通过。配套`.moef` SHA256为`e1c0cff1dd3541ed97cab2cb39a76ad4f28dc659b3be7991cc7ac92dd8ea090b`,和TIFF一起同步到正式模板目录并逐字节核对。本次临时/旧APP及公证ZIP已清理;扫描范围和回执在`docs/verification/v1.1.0/`。最终恢复标签为`v1.1.0`,后续记录提交不改变签名产品源码f79d24e。原生UI和视觉仍由用户自测。
交付偏好(2026-10-21用户明确更新):后续本产品安装需释放Motion时,可直接结束Motion进程,不再重复请求保存退出;用户说明不在Motion里进行工作。保持不自动操作Motion/FCP界面的约定。本次检查时两个宿主已经退出,没有执行强退。
+206 -51
查看文件
@@ -1,9 +1,28 @@
{ {
"version": "1.0.2", "version": "1.1.0",
"pluginUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241", "pluginUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
"filterObject": "510000005", "filterObject": "510000005",
"visiblePublished": 46, "visiblePublished": 1,
"parameters": [ "parameters": [
{
"id": 210,
"name": "",
"channel": "./210",
"kind": "custom-archive",
"class": "PGInspectorState",
"published": true,
"hasEdits": false,
"schema": 1,
"templateFlags": "12901679104",
"panelCards": [
"磨皮",
"美白",
"瘦脸"
],
"secondarySection": "更多",
"valueStorage": "Own secure keyed archive; Custom Data inside BlindDataObject, DataIsLegacy=false",
"nativeLoadingValidation": "pending user"
},
{ {
"id": 70, "id": 70,
"name": "启用磨皮", "name": "启用磨皮",
@@ -12,7 +31,9 @@
"kind": "bool", "kind": "bool",
"default": 1, "default": 1,
"value": 1, "value": 1,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 1, "id": 1,
@@ -22,7 +43,9 @@
"kind": "float", "kind": "float",
"default": 1, "default": 1,
"value": 1, "value": 1,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 12, "id": 12,
@@ -32,7 +55,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0.5, "value": 0.5,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 3, "id": 3,
@@ -42,7 +67,9 @@
"kind": "float", "kind": "float",
"default": 8, "default": 8,
"value": 8, "value": 8,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 30, "id": 30,
@@ -52,7 +79,9 @@
"kind": "bool", "kind": "bool",
"default": 1, "default": 1,
"value": 1, "value": 1,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 71, "id": 71,
@@ -62,7 +91,9 @@
"kind": "bool", "kind": "bool",
"default": 1, "default": 1,
"value": 1, "value": 1,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 42, "id": 42,
@@ -72,7 +103,9 @@
"kind": "popup", "kind": "popup",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12901744656"
}, },
{ {
"id": 2, "id": 2,
@@ -82,7 +115,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 14, "id": 14,
@@ -92,7 +127,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 15, "id": 15,
@@ -102,7 +139,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 41, "id": 41,
@@ -112,7 +151,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 31, "id": 31,
@@ -122,7 +163,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 32, "id": 32,
@@ -132,7 +175,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 73, "id": 73,
@@ -142,7 +187,9 @@
"kind": "bool", "kind": "bool",
"default": 1, "default": 1,
"value": 1, "value": 1,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 17, "id": 17,
@@ -152,7 +199,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 4, "id": 4,
@@ -162,7 +211,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 72, "id": 72,
@@ -172,7 +223,9 @@
"kind": "bool", "kind": "bool",
"default": 1, "default": 1,
"value": 1, "value": 1,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 18, "id": 18,
@@ -182,7 +235,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 33, "id": 33,
@@ -192,7 +247,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 22, "id": 22,
@@ -202,7 +259,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 23, "id": 23,
@@ -212,7 +271,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 24, "id": 24,
@@ -222,7 +283,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 25, "id": 25,
@@ -232,7 +295,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 26, "id": 26,
@@ -242,7 +307,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 27, "id": 27,
@@ -252,7 +319,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 28, "id": 28,
@@ -262,7 +331,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 29, "id": 29,
@@ -272,7 +343,9 @@
"kind": "float", "kind": "float",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 20, "id": 20,
@@ -282,7 +355,9 @@
"kind": "bool", "kind": "bool",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 21, "id": 21,
@@ -292,7 +367,9 @@
"kind": "point", "kind": "point",
"default": 0.5, "default": 0.5,
"value": 0.5, "value": 0.5,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905943072"
}, },
{ {
"id": 39, "id": 39,
@@ -302,7 +379,9 @@
"kind": "bool", "kind": "bool",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 40, "id": 40,
@@ -312,7 +391,9 @@
"kind": "float", "kind": "float",
"default": 1.8, "default": 1.8,
"value": 1.8, "value": 1.8,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 45, "id": 45,
@@ -322,7 +403,9 @@
"kind": "bool", "kind": "bool",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 46, "id": 46,
@@ -332,7 +415,9 @@
"kind": "popup", "kind": "popup",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12901744656"
}, },
{ {
"id": 50, "id": 50,
@@ -342,7 +427,9 @@
"kind": "button", "kind": "button",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": null
}, },
{ {
"id": 51, "id": 51,
@@ -352,7 +439,9 @@
"kind": "button", "kind": "button",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": null
}, },
{ {
"id": 52, "id": 52,
@@ -362,7 +451,9 @@
"kind": "button", "kind": "button",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": null
}, },
{ {
"id": 53, "id": 53,
@@ -372,7 +463,9 @@
"kind": "status", "kind": "status",
"default": "选择画面中的人脸后开始分析", "default": "选择画面中的人脸后开始分析",
"value": "选择画面中的人脸后开始分析", "value": "选择画面中的人脸后开始分析",
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 19, "id": 19,
@@ -382,7 +475,9 @@
"kind": "bool", "kind": "bool",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 35, "id": 35,
@@ -392,7 +487,9 @@
"kind": "bool", "kind": "bool",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 7, "id": 7,
@@ -402,7 +499,9 @@
"kind": "float", "kind": "float",
"default": 148, "default": 148,
"value": 148, "value": 148,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 8, "id": 8,
@@ -412,7 +511,9 @@
"kind": "float", "kind": "float",
"default": 0.6, "default": 0.6,
"value": 0.6, "value": 0.6,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 9, "id": 9,
@@ -422,7 +523,9 @@
"kind": "float", "kind": "float",
"default": 0.4, "default": 0.4,
"value": 0.4, "value": 0.4,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 10, "id": 10,
@@ -432,7 +535,9 @@
"kind": "bool", "kind": "bool",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 43, "id": 43,
@@ -442,7 +547,9 @@
"kind": "bool", "kind": "bool",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 44, "id": 44,
@@ -452,7 +559,9 @@
"kind": "popup", "kind": "popup",
"default": 0, "default": 0,
"value": 0, "value": 0,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12901744656"
}, },
{ {
"id": 5, "id": 5,
@@ -462,7 +571,9 @@
"kind": "float", "kind": "float",
"default": 1, "default": 1,
"value": 1, "value": 1,
"published": true "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 60, "id": 60,
@@ -470,7 +581,9 @@
"channel": "./60", "channel": "./60",
"kind": "hidden-string", "kind": "hidden-string",
"value": "", "value": "",
"published": false "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 61, "id": 61,
@@ -478,7 +591,9 @@
"channel": "./61", "channel": "./61",
"kind": "hidden-string", "kind": "hidden-string",
"value": "", "value": "",
"published": false "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
}, },
{ {
"id": 62, "id": 62,
@@ -486,7 +601,9 @@
"channel": "./62", "channel": "./62",
"kind": "hidden-string", "kind": "hidden-string",
"value": "", "value": "",
"published": false "published": false,
"hiddenCompatibility": true,
"templateFlags": "12905938976"
} }
], ],
"effectSource": { "effectSource": {
@@ -498,5 +615,43 @@
"dropZone": 1, "dropZone": 1,
"path": "Drop Zone.tiff", "path": "Drop Zone.tiff",
"resourceBundled": true "resourceBundled": true
},
"informationalPublished": 2,
"totalPublished": 3,
"legacyDefinitionCount": 49,
"legacyGroupsPreserved": [
100,
101,
102,
103,
104,
105
],
"nativeHiddenPolicy": "All legacy groups and parameters remain registered with SDK kFxParameterFlag_HIDDEN. Known scalar/point/string hidden XML flags are copied from observed native structures; group/popup/button private flags are preserved.",
"publishSettings": [
{
"object": "510000011",
"channel": "./2/100",
"name": "FCP 版本作者→"
},
{
"object": "510000012",
"channel": "./2/100",
"name": "更多模板浏览器访问→"
},
{
"object": "510000005",
"channel": "./210",
"name": ""
}
],
"defaults": {
"legacyMidtoneDefault": 0,
"legacyMidtoneValue": 0.5,
"customStateHasEdits": false,
"initialEffectiveMidtone": 0.5,
"publishOSC": 0,
"manualAnalysisVisible": false,
"trackingMode": "automatic primary realtime follow"
} }
} }
+28 -18
查看文件
@@ -1,33 +1,43 @@
# 大鱼人像美化 FCP 效果模板 # 大鱼人像美化 FCP 效果模板
版本:1.0.4。滤镜 UUID:`FB14B0E6-59B2-4836-916F-CD0C559E9241`。模板必须与已安装的大鱼人像美化 FxPlug APP 配套使用。 版本:1.1.0。滤镜 UUID:`FB14B0E6-59B2-4836-916F-CD0C559E9241`。需配套同版大鱼人像美化 FxPlug APP。
## 默认数值 ## 三卡片与自动跟随
模板首次应用时,“中频磨皮”为 **50%(0.5)**;直接在 Motion 添加裸 FxPlug 滤镜时为 **0%(0)**。模板保存了 `default=0 / value=0.5`,保留裸插件的重置基准;其他功能参数与插件默认一致。整体混合为100%,脸型、肤色提亮、锐化、牙齿和眼睛提亮初始为0,跟踪初始关闭。此模板不预置人脸身份或跟踪记录。 FCP 发布列表仅保留作者、网站两个空绑定展示项,以及一个空名称的自定义面板入口 `./210`,共3项。面板内提供“磨皮、美白、瘦脸”三张卡片和“更多”附加设置。主体人脸由当前画面自动选择并实时跟随,无需手动前向/后向分析。多人模式、预览框、防跳脸与各项调节由插件自定义面板承载。
## 参数与输入 原有46个平铺功能入口已撤下。旧49个参数定义及六组路径仍保留供旧项目兼容,插件以 SDK 隐藏标记注册;旧手动分析按钮、跟踪目标/状态、选脸点和目标数据不再发布为界面入口。隐藏的目标数据60/61/62不删除。发布 OSC 初始为0;旧OSC类注册由插件保留。
- 保留磨皮、肤色、细节、脸型、人脸选择与跟踪、蒙版与输出六个原生 FxPlug 参数组,发布46项可见功能参数,包括前向/后向分析及清除按钮。 ## 默认与旧项目
- 目标1–3的隐藏分析数据以参数60/61/62保留在滤镜节点中,不发布成用户控件;分析完成后由插件写入宿主参数并随工程保存。
- 选脸位置发布为原生二维参数,并启用滤镜的“发布 OSC”宿主通道。跟踪按钮、OSC与工程关闭重开后的数据恢复须由实际 FCP 验证。
- 画面只含一个标准 Effect Source(Type=3、Drop Zone=1),其 Source Media 指向 clip `510000006`,clip 位于 footage `510000007`。`pathURL=Drop Zone.tiff` 为模板同目录内的相对引用,标准1200×1200占位图已随模板附带,没有外部安装目录依赖、示例人像、旧滤镜或新动画。FCP时间线片段的实际替换已在独立验收项目中确认正常出图。
- 作者与网站为两个原生空绑定展示菜单,不改变渲染。
建议模板安装位置:`~/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/`。APP正式安装与模板安装、Motion/FCP原生验收由主任务处理。 模板首次应用时,旧原生“中频磨皮”仍保存 `default=0 / value=0.5`,即初始局部磨皮50%;直接添加裸插件时为0%。自定义 `PGInspectorState` 初始 `hasEdits=NO`,因此读取并保留旧原生参数,不会用面板归档中的默认0覆盖模板0.5或旧项目数值。面板经过编辑后由自定义状态持久保存调节值。此模板不预置任何人脸身份或跟踪记录。
## 结构来源与静态验证 ## 自有状态归档
Apple项目、群组、Effect Source和滤镜节点骨架来源于用户自有“大鱼去黑.moef”;仅保留这些原生结构,已替换滤镜UUID、名称、版本和全部功能参数。FxPlug组/字符串/点参数的原生结构交叉参照自有Number与Zoom and Pan样本;作者/网站空绑定来自ae-to-motion技能的原生版权样本。 参数210使用本插件生成的 `PGInspectorState` 安全键控归档:内部 `Custom Data` 对象、外部 `BlindDataObject` 与 `DataIsLegacy=false`,再按已核对的Motion文本编码和长度封装。保存 `schema=1 / edited=false`,不使用原版私有数据、类名、品牌资源或空 `<dataValue>` 猜测初始化。默认和当前归档相同,可完整解码并往返核验。
`PARAMETERS.json`逐项记录参数ID、组、发布通道及初始值;`VALIDATION.json`记录本次静态核验。48个发布路径(46功能+2信息)均能反查实际对象和通道,实体ID无冲突,三个隐藏数据通道未发布;必要占位媒体以相对路径随包附带,保存选择状态为0个内容对象且焦点为项目。技能`motion_selection.py check`与`audit_motion.py --require-no-selection`通过。 该编码依据原生文件静态结构及独立Foundation归档检查;1.1.0自定义面板在Motion/FCP中的实际创建、显示、交互、撤销和关闭重开恢复仍待用户验收,不能以XML可解析或归档解码成功代替宿主加载通过。
结构核验之外,已用实际素材在FCP和Motion完成加载、画面及提亮回归测试。详细范围及尚未覆盖的边界见 [验证记录](../../docs/验证记录.md)。 ## 输入、媒体与版权
## 前版输入链错误与本次修复 输入链沿用已修复的标准 Effect Source(Type=3、Drop Zone=1):Source Media→clip `510000006`→footage `510000007`→相对文件 `Drop Zone.tiff`。该占位图随模板附带,420128字节,SHA256 `a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e`。没有外部素材、示例人像或新动画。
前版生成稿(SHA256 `3b7984e11a92487834ab67121fd3814bf13c22b77a70851298efa84206f91478`)将效果源的 Source Media 设为0,并删除了原生clip/footage。用户在FCP应用后实测整幅黑屏。这是模板装配错误;保留Type=3本身并不能证明FCP输入替换链完整。此前XML解析、发布路径和无缺媒体检查通过,也没有覆盖真实FCP画面输入,不能当作该模板已经可用的证据。 作者与网站仍是同一个Rig下的两个无受控参数菜单:“FCP 版本作者→/大鱼先森Media”和“更多模板浏览器访问→/dayu.media”。仅展示信息,不改变渲染。
黑屏修复恢复标准效果源clip/footage和Source Media关联,并随包加入标准`Drop Zone.tiff`,保留49个插件参数、48个发布入口与Type=3/Drop Zone=1。1.0.4同时包含独立的提亮蒙版与选脸坐标修复。该占位图从Apple Motion的Ozone.framework标准资源复制,实际运行只引用本模板内的相对副本。图像SHA256为 `a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e`,420128字节。 正式安装位置:`~/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/`。1.1.0模板和占位TIFF已同步并逐字节核验,配套APP已正式签名、公证、安装;没有操作Motion/FCP界面,用户自测原生面板。
本次已经静态核验Source Media→clip→相对文件链存在、文件字节一致、48项发布仍解析、隐藏数据未发布和保存选择状态;修复后已在FCP独立验收项目新增实例,确认输入替换、实际人像和提亮画面正常;Mix=0逐字节直通由宿主适配器IOSurface回归测试覆盖,未将其混称为FCP截图逐像素核验。 ## 验证边界与历史修复
`PARAMETERS.json`记录新旧参数和3条发布路径;`VALIDATION.json`分开记录结构/归档核验与当前宿主待验项。内容选择状态保持为空,效果源、clip/footage、占位资源和版权绑定结构均保留。
早期模板曾误删clip/footage、将Source Media设为0,用户在FCP实测黑屏;随后恢复完整标准输入链,历史版本已实际出图。该历史结果只支持输入链,不能证明新1.1.0自定义面板已加载。1.0.5加入76点17组局部瘦脸;1.0.6移除IOSurface重复翻转并修复下巴三组局部位移。旧未标记坐标空间的跟踪数据保留但不重放,自动回退实时跟随。
## 可复现的只读核验
在仓库根目录执行:
```sh
python3 scripts/inspector-template.py audit "Templates/大鱼人像美化/大鱼人像美化.moef"
```
脚本可提取内外归档并核验自有类、schema、初始状态、3条发布和媒体引用;`encode --inner <本插件生成的Custom Data归档> --text-output <输出文件>` 可重新包装文本值,不启动宿主。其结果明确保留宿主加载待验状态。
+180 -6
查看文件
@@ -1,11 +1,11 @@
{ {
"version": "1.0.4", "version": "1.1.0",
"sha256": "d234d504eba9e6d37b910b622a46c92458e658c5bbdad2caf5831c2a1463a3e2", "sha256": "e1c0cff1dd3541ed97cab2cb39a76ad4f28dc659b3be7991cc7ac92dd8ea090b",
"filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241", "filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
"publishedFunctionalControls": 46, "publishedFunctionalControls": 1,
"informationalControls": 2, "informationalControls": 2,
"hiddenTrackingSlots": 3, "hiddenTrackingSlots": 3,
"publishedPathsResolved": 48, "publishedPathsResolved": 3,
"sourceType": 3, "sourceType": 3,
"sourceMedia": "510000006", "sourceMedia": "510000006",
"clip": "510000006", "clip": "510000006",
@@ -23,10 +23,184 @@
"problem": "Previous generated template removed the standard source clip and could not establish the native effect-source input chain. XML/reference checks did not validate FCP timeline replacement.", "problem": "Previous generated template removed the standard source clip and could not establish the native effect-source input chain. XML/reference checks did not validate FCP timeline replacement.",
"change": "Restored the user-owned reference template clip/footage skeleton with new unique IDs; restored the Source Media link; bundled standard Drop Zone.tiff; filter, publications and Type=3/Drop Zone=1 preserved." "change": "Restored the user-owned reference template clip/footage skeleton with new unique IDs; restored the Source Media link; bundled standard Drop Zone.tiff; filter, publications and Type=3/Drop Zone=1 preserved."
}, },
"hostValidation": { "slimValidation": {
"offlineFullChecks": 604,
"slimChecks": 224,
"extraRealVideoASanChecks": 264,
"sampledVideoFrames": 11,
"maxUVOracleError": 1.09828022e-05
},
"coordinateChinValidation": {
"offlineFullChecks": 904,
"thinChinChecks": 394,
"hostAdapterChecksASan": 247,
"oscChecksASan": 98,
"extraRealVideoASanChecks": 494,
"sampledVideoFrames": 11,
"independentAnatomicalChecks": 12,
"originCombinations": 16,
"allOriginTagsPixelIdentical": true,
"maxByteErrorVsIndependentCIReference": 2,
"maxUVOracleError": 1.09828022e-05,
"nativeVisualQA": "pending user"
},
"historicalHostValidation": {
"date": "2026-10-21", "date": "2026-10-21",
"FCP": "Restored input-chain template shows supplied real video; effect parameters and forward analysis responded in independent QA project.", "FCP": "Restored input-chain template shows supplied real video; effect parameters and forward analysis responded in independent QA project.",
"Motion": "1.0.4 installed formal app loaded the real-video QA project and rendered brightness/geometry with a primary box at different playhead positions; offline tracking disabled.", "Motion": "1.0.4 installed formal app loaded the real-video QA project and rendered brightness/geometry with a primary box at different playhead positions; offline tracking disabled.",
"scope": "Original input-chain/brightness native regression passed; 1.0.4 FCP automatic-follow and explicit OSC native click tests remain pending because user took over Motion/FCP self-testing. Offline 380 checks passed; no exhaustive occlusion/identity equivalence claim." "scope": "1.0.6 coordinate/chin repair native visual QA is reserved for user self-testing; no Motion/FCP UI operations. Historical host tests remain scoped to their versions.",
"version105": "Formal 1.0.5 (7) signed/notarized/stapled/installed and PluginKit formal path verified. User closed hosts before replacement; no Motion/FCP UI operated. Native visual testing pending user.",
"version106": "Formal 1.0.6 (8) universal Release signed/notarized/stapled/installed, identity/Gatekeeper/formal PluginKit path verified. User closed hosts; companion moef and TIFF synchronized and byte-verified. Generated outer APPs and notarization ZIP removed. No Motion/FCP UI operations; native visual QA pending user."
},
"totalPublished": 3,
"hiddenCompatibilityParameters": 49,
"legacyGroupPathsPreserved": true,
"legacyValuesPreserved": true,
"publishOSC": 0,
"currentHostValidation": {
"version": "1.1.0",
"Motion": "pending user",
"FCP": "pending user",
"scope": "No Motion/FCP launch, quit, activation, UI operation or native save was performed. XML structure and standalone archive checks do not establish custom inspector loading."
},
"customParameter": {
"id": 210,
"channel": "./210",
"name": "",
"flags": "12901679104",
"flagsOrigin": "Observed original custom parameter native structure, without copying its data blob",
"class": "PGInspectorState",
"schema": 1,
"hasEdits": false,
"valueCount": 30,
"disabledCount": 0,
"classes": [
"NSDictionary",
"NSSet",
"PGInspectorState"
],
"innerBytes": 935,
"innerSHA256": "257540ac6079756a76f3d31dff1b9bca2b33393962057927791316965495ab60",
"outerBytes": 1127,
"outerSHA256": "95d5f1e2c35c40e9f1234b1480894bcc0ee68c0822c9bd3f7c04338062e6a26b",
"encodedCharacters": 1516,
"encodingAlphabet": "*-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
"envelope": "8-byte big-endian outer archive length, byte 0x2a, outer NSKeyedArchiver payload, zero padding to 3-byte boundary, custom base64 alphabet",
"outerTopKeys": [
"BlindDataObject",
"DataIsLegacy"
],
"innerTopKey": "Custom Data",
"legacyFlag": false,
"defaultEqualsCurrent": true,
"exactEncodeDecodeRoundtrip": true,
"outerSecureDecodeRoundtrip": true,
"vendorClassesOrBlobReused": false,
"emptyDataValueFallback": "not established and not relied upon",
"nativeLoadStatus": "pending user"
},
"publicationTransition": {
"previousFunctionalControls": 46,
"currentFunctionalControls": 1,
"informationControls": 2,
"manualAnalysisPublished": false,
"retainedForCompatibility": [
45,
46,
50,
51,
52,
53,
60,
61,
62
],
"oldParameterPathsUnchanged": true
},
"xmlFlagPolicy": {
"scalarBooleanString": "12905938976; complete observed hidden native flags",
"point": "12905943072; complete observed hidden native point flags",
"groupPopupButton": "existing private XML flags retained; no guessed bit patch",
"pluginHiddenRegistration": "verified in current source: kFxParameterFlag_HIDDEN for all legacy groups, numeric/point/popup/string parameters; commands additionally disabled"
},
"nonVisualAcceptance": {
"XMLAndReferences": "pass: own codec audit and ae-to-motion audit_motion.py",
"mediaClosure": "pass: Source Media/clip/footage/relative TIFF and media SHA256",
"publications": "pass: exactly two copyright menus plus empty-name ./210",
"copyrightBindings": "pass: complete Rig/widget trees unchanged from baseline, own audit resolves menus/publications",
"selection": "pass: 0 selected content objects; project focus",
"customArchiveStructure": "pass: exact inner bytes, own class/schema/values; Foundation outer secure decode",
"customHostInstantiation": "pending user"
},
"semanticPreservation": {
"inverseDiffPassed": true,
"allowedChanges": [
"description/version",
"46 native functional publications replaced by custom210",
"custom210 own state archive",
"observed scalar/point/string hidden flags",
"publish OSC value 1 to 0"
],
"otherStructureAndLegacyValuesUnchanged": true
},
"reproducibleCodec": {
"script": "../../scripts/inspector-template.py",
"commands": [
"python3 scripts/inspector-template.py audit \"Templates/大鱼人像美化/大鱼人像美化.moef\"",
"python3 scripts/inspector-template.py encode --inner build/ui-analysis/PGInspectorState-default-inner.bplist --text-output build/ui-analysis/panel-value.txt"
],
"ownRoundtrip": "pass",
"malformedOrVendorArchiveRejectionCases": 5,
"foundationOuterSecureDecode": "pass",
"innerClassDecode": "model generates own archive; structure decoded and exact generated bytes preserved; native host instantiation pending user"
},
"automatedInspectorValidation": {
"scope": "offscreen AppKit inspector, real production controller and mocked host callbacks; no Motion/FCP UI",
"fullChecks": 1039,
"counts": {
"beauty": 49,
"face": 116,
"thinChin": 394,
"hostAdapterASan": 248,
"oscASan": 98,
"inspectorASan": 134
},
"nativeVisualQA": "pending user",
"manualAnalysisUI": "removed; legacy IDs hidden and buttons disabled",
"renderMode": "realtime automatic follow; stored manual geometry never used",
"templatePublishedEntries": 3,
"offscreenWidths": [
280,
320,
380,
520
],
"independentViewChecks": 27,
"fixedRegressions": [
"shape group bypass clears face-box overlay",
"disabled legacy detail effects stay disabled on first edit",
"legacy global smoothing remains enabled when its old local group was off",
"new gesture reads current host after immediate undo",
"pending synchronization is bound to CMTime",
"same-time host change refreshes without waiting",
"explicit numeric input is never skipped due to stale UI cache"
],
"archives": "Own PGInspectorState secure payload; Foundation outer and inner decode and Motion alphabet roundtrip tested"
},
"installation": {
"version": "1.1.0",
"build": "9",
"status": "installed",
"appPath": "/Library/Plug-Ins/FxPlug/beatygril.app",
"templateAndSourceResourceByteVerified": true,
"signatureVerified": true,
"notarization": "Accepted",
"stapleValidated": true,
"gatekeeperAccepted": true,
"formalPluginKitPathVerified": true,
"UIOperated": false,
"hostTerminationPerformed": false,
"nativeLoadAndVisualQA": "pending user",
"receipt": "../../docs/verification/v1.1.0/installed-verification.json"
} }
} }
@@ -46,7 +46,7 @@
<flags>1</flags> <flags>1</flags>
</template> </template>
<build /> <build />
<description>大鱼人像美化 1.0.4:人像磨皮、肤色、脸型、五官保护和三目标人脸跟踪。模板中频磨皮默认为50%。</description> <description>大鱼人像美化 1.1.0:磨皮、美白、瘦脸三卡片自定义面板,自动选择主体并实时跟随。模板局部磨皮默认为50%。</description>
<scene> <scene>
<sceneSettings> <sceneSettings>
<width>1920</width> <width>1920</width>
@@ -82,52 +82,7 @@
<version>2</version> <version>2</version>
<target object="510000011" channel="./2/100" name="FCP 版本作者→" /> <target object="510000011" channel="./2/100" name="FCP 版本作者→" />
<target object="510000012" channel="./2/100" name="更多模板浏览器访问→" /> <target object="510000012" channel="./2/100" name="更多模板浏览器访问→" />
<target object="510000005" channel="./100/70" name="启用磨皮" /> <target object="510000005" channel="./210" name="" />
<target object="510000005" channel="./100/1" name="磨皮强度" />
<target object="510000005" channel="./100/12" name="中频磨皮" />
<target object="510000005" channel="./100/3" name="磨皮半径" />
<target object="510000005" channel="./100/30" name="保护五官" />
<target object="510000005" channel="./101/71" name="启用肤色" />
<target object="510000005" channel="./101/42" name="肤色风格" />
<target object="510000005" channel="./101/2" name="肤色提亮" />
<target object="510000005" channel="./101/14" name="肤色色相" />
<target object="510000005" channel="./101/15" name="肤色饱和度" />
<target object="510000005" channel="./101/41" name="均匀肤色" />
<target object="510000005" channel="./101/31" name="牙齿美白" />
<target object="510000005" channel="./101/32" name="眼睛提亮" />
<target object="510000005" channel="./102/73" name="启用细节" />
<target object="510000005" channel="./102/17" name="高频细节平滑" />
<target object="510000005" channel="./102/4" name="锐化" />
<target object="510000005" channel="./103/72" name="启用脸型" />
<target object="510000005" channel="./103/18" name="瘦脸" />
<target object="510000005" channel="./103/33" name="大眼" />
<target object="510000005" channel="./103/22" name="短脸" />
<target object="510000005" channel="./103/23" name="窄脸" />
<target object="510000005" channel="./103/24" name="下巴" />
<target object="510000005" channel="./103/25" name="V 脸" />
<target object="510000005" channel="./103/26" name="下颌" />
<target object="510000005" channel="./103/27" name="颧骨" />
<target object="510000005" channel="./103/28" name="小头" />
<target object="510000005" channel="./103/29" name="小头范围" />
<target object="510000005" channel="./104/20" name="只处理选中人脸" />
<target object="510000005" channel="./104/21" name="选脸位置" />
<target object="510000005" channel="./104/39" name="多人高速模式" />
<target object="510000005" channel="./104/40" name="防跳脸范围" />
<target object="510000005" channel="./104/45" name="使用跟踪数据" />
<target object="510000005" channel="./104/46" name="跟踪目标" />
<target object="510000005" channel="./104/50" name="向前分析" />
<target object="510000005" channel="./104/51" name="向后分析" />
<target object="510000005" channel="./104/52" name="清除当前目标" />
<target object="510000005" channel="./104/53" name="跟踪状态" />
<target object="510000005" channel="./104/19" name="显示人脸框" />
<target object="510000005" channel="./104/35" name="显示跟踪框" />
<target object="510000005" channel="./105/7" name="蒙版阈值" />
<target object="510000005" channel="./105/8" name="蒙版柔度" />
<target object="510000005" channel="./105/9" name="边缘保护" />
<target object="510000005" channel="./105/10" name="预览蒙版" />
<target object="510000005" channel="./105/43" name="反转蒙版" />
<target object="510000005" channel="./105/44" name="蒙版通道" />
<target object="510000005" channel="./105/5" name="整体混合" />
</publishSettings> </publishSettings>
<currentFrame>0 1 1 0</currentFrame> <currentFrame>0 1 1 0</currentFrame>
<currentObject>510000001</currentObject> <currentObject>510000001</currentObject>
@@ -298,72 +253,81 @@
<parameter name="Width" id="313" flags="8589934610" default="1" value="1920" /> <parameter name="Width" id="313" flags="8589934610" default="1" value="1920" />
<parameter name="Height" id="314" flags="8589934610" default="1" value="1080" /> <parameter name="Height" id="314" flags="8589934610" default="1" value="1080" />
</parameter> </parameter>
<filter name="大鱼人像美化" id="510000005" factoryID="6" pluginUUID="FB14B0E6-59B2-4836-916F-CD0C559E9241" pluginVersion="1.0.4" pluginName="大鱼人像美化" pluginDynamicParams="0"> <filter name="大鱼人像美化" id="510000005" factoryID="6" pluginUUID="FB14B0E6-59B2-4836-916F-CD0C559E9241" pluginVersion="1.1.0" pluginName="大鱼人像美化" pluginDynamicParams="0">
<timing in="0 1 1 0" out="299 30 1 0" offset="0 1 1 0" /> <timing in="0 1 1 0" out="299 30 1 0" offset="0 1 1 0" />
<baseFlags>8589934608</baseFlags> <baseFlags>8589934608</baseFlags>
<parameter name="" id="210" flags="12901679104">
<numberOfKeypoints>1</numberOfKeypoints>
<keypoint>
<time>0 0 0 0</time>
<value />
</keypoint>
<defaultVal>********-4QeMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl21dq7kP4ZnR1*kp*20*kE3-UQ8K0FqNL7nOKxiKGFVQaBcOLNZQZEYR4xkK0FjMadZMrFn2U*-Ve-T2*xCIohZSKJYEL7XO4ZqNL9F0*ZPErJnR4xh62FVR44**OwE8EgA3pZOKplRLZxUMK7XN4JaNqVdOahgPKtjQ53mQrFpRbRsSLdvUMG5JGFiRKlgpEoC1l*F2VAI3FNKRa3gRKJnJWFXP43nQpNZN4ZoNKFKQqBcNKpVK4FdQq3WP4JYU*8*8*UE*M*aolUN1VctK3RCImtfNLZnKYtH9axWOaJXR5Cj2-sP5-oS5m*V6WAY7GMb80Ye8mkh9WwkAH6nB1IqBnW**s*2U*K*-c*5U*W*0M*8U*i*16*BU*u*1s*EU-4*2c*HU-G*3M*KU-S*46*NU-e*4s*QU-q*5c*TU00j2-suCnguCncuCXd1CncuCXcuCXcuCncvCXcuCXdJCXe*6M*WU08*6M*WU04*6M*VU04*6s*WU04*6M*VU04*6M*VU04*6M*WU04*6c*VU04*6M*VU04*76*VU04*7F*d2-sEFl*P2*IE6l*M2*6E6-*c22ME2V*R2-cE1l*22-QE1-*e22UE5l*-20QE5-*F2-YE1V*1202E3WA**********0Azw********0AzzAnAk****0B*6********B7wTLtzKWFXP43nQqtVPKJM74BgMLBnNLBQHZB2OKBoOKxiML7tcbu*K2tHHq7eNKBooVYCUcCUU0TGT5q3VZJCIpBZR883UB7wTMW7Ll*EI2R7PbBkNKBoPr7HR43oNO88U3wE23-5GKtnQ4JXR4xmIrFVR4I*0**F*-c*7**d*16*Bk-7*2k*K*-O*6M*X*0L*7s*dE0g*9A*j*0y*A**kE11*AI*n*1I*Bw-**20*EE--U26*Ec-1*2C*F*-2U2I*FM-4*2O*Fk-5U2U*G6-7*2a*GU-8U2g*Gs-A*2m*HE-BU2s*Hc-D*3R*Jw-ME3X*KI-Nk3d*Kg-PE3j*L2-Qk3p*LQ-SE3v*Lo-Tk4-*MA-VE45*MY-Wk4B*Mw-YE4H*NI-Zk4N*Ng-bE4T*O2-ck4Z*OQ-eE4f*Oo-fk4l*PA-hE4r*PY-ik4x*Pw-kE51*QI-lk57*Qg-nE5D*R2-ok5J*RQ-s*5d*T6-yk6**Ug03*6V*WE09E6m*XA0BE6u*Y*0Ek76*Zg0LU********6-*********6g*******************7l0**6*-2*4U*Y*0Y*AU*r*2Y*HU-P*4o*Pk-l*5I*SkEa*********U2*********2*******************-0Q*</defaultVal>
<dataValue>********-4QeMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl21dq7kP4ZnR1*kp*20*kE3-UQ8K0FqNL7nOKxiKGFVQaBcOLNZQZEYR4xkK0FjMadZMrFn2U*-Ve-T2*xCIohZSKJYEL7XO4ZqNL9F0*ZPErJnR4xh62FVR44**OwE8EgA3pZOKplRLZxUMK7XN4JaNqVdOahgPKtjQ53mQrFpRbRsSLdvUMG5JGFiRKlgpEoC1l*F2VAI3FNKRa3gRKJnJWFXP43nQpNZN4ZoNKFKQqBcNKpVK4FdQq3WP4JYU*8*8*UE*M*aolUN1VctK3RCImtfNLZnKYtH9axWOaJXR5Cj2-sP5-oS5m*V6WAY7GMb80Ye8mkh9WwkAH6nB1IqBnW**s*2U*K*-c*5U*W*0M*8U*i*16*BU*u*1s*EU-4*2c*HU-G*3M*KU-S*46*NU-e*4s*QU-q*5c*TU00j2-suCnguCncuCXd1CncuCXcuCXcuCncvCXcuCXdJCXe*6M*WU08*6M*WU04*6M*VU04*6s*WU04*6M*VU04*6M*VU04*6M*WU04*6c*VU04*6M*VU04*76*VU04*7F*d2-sEFl*P2*IE6l*M2*6E6-*c22ME2V*R2-cE1l*22-QE1-*e22UE5l*-20QE5-*F2-YE1V*1202E3WA**********0Azw********0AzzAnAk****0B*6********B7wTLtzKWFXP43nQqtVPKJM74BgMLBnNLBQHZB2OKBoOKxiML7tcbu*K2tHHq7eNKBooVYCUcCUU0TGT5q3VZJCIpBZR883UB7wTMW7Ll*EI2R7PbBkNKBoPr7HR43oNO88U3wE23-5GKtnQ4JXR4xmIrFVR4I*0**F*-c*7**d*16*Bk-7*2k*K*-O*6M*X*0L*7s*dE0g*9A*j*0y*A**kE11*AI*n*1I*Bw-**20*EE--U26*Ec-1*2C*F*-2U2I*FM-4*2O*Fk-5U2U*G6-7*2a*GU-8U2g*Gs-A*2m*HE-BU2s*Hc-D*3R*Jw-ME3X*KI-Nk3d*Kg-PE3j*L2-Qk3p*LQ-SE3v*Lo-Tk4-*MA-VE45*MY-Wk4B*Mw-YE4H*NI-Zk4N*Ng-bE4T*O2-ck4Z*OQ-eE4f*Oo-fk4l*PA-hE4r*PY-ik4x*Pw-kE51*QI-lk57*Qg-nE5D*R2-ok5J*RQ-s*5d*T6-yk6**Ug03*6V*WE09E6m*XA0BE6u*Y*0Ek76*Zg0LU********6-*********6g*******************7l0**6*-2*4U*Y*0Y*AU*r*2Y*HU-P*4o*Pk-l*5I*SkEa*********U2*********2*******************-0Q*</dataValue>
</parameter>
<parameter name="磨皮" id="100" flags="8589938704"> <parameter name="磨皮" id="100" flags="8589938704">
<parameter name="启用磨皮" id="70" flags="12901679120" default="1" value="1" /> <parameter name="启用磨皮" id="70" flags="12905938976" default="1" value="1" />
<parameter name="磨皮强度" id="1" flags="12901679120" default="1" value="1" /> <parameter name="磨皮强度" id="1" flags="12905938976" default="1" value="1" />
<parameter name="中频磨皮" id="12" flags="12901679120" default="0" value="0.5" /> <parameter name="中频磨皮" id="12" flags="12905938976" default="0" value="0.5" />
<parameter name="磨皮半径" id="3" flags="12901679120" default="8" value="8" /> <parameter name="磨皮半径" id="3" flags="12905938976" default="8" value="8" />
<parameter name="保护五官" id="30" flags="12901679120" default="1" value="1" /> <parameter name="保护五官" id="30" flags="12905938976" default="1" value="1" />
</parameter> </parameter>
<parameter name="肤色" id="101" flags="8589938704"> <parameter name="肤色" id="101" flags="8589938704">
<parameter name="启用肤色" id="71" flags="12901679120" default="1" value="1" /> <parameter name="启用肤色" id="71" flags="12905938976" default="1" value="1" />
<parameter name="肤色风格" id="42" flags="12901744656" default="0" value="0" /> <parameter name="肤色风格" id="42" flags="12901744656" default="0" value="0" />
<parameter name="肤色提亮" id="2" flags="12901679120" default="0" value="0" /> <parameter name="肤色提亮" id="2" flags="12905938976" default="0" value="0" />
<parameter name="肤色色相" id="14" flags="12901679120" default="0" value="0" /> <parameter name="肤色色相" id="14" flags="12905938976" default="0" value="0" />
<parameter name="肤色饱和度" id="15" flags="12901679120" default="0" value="0" /> <parameter name="肤色饱和度" id="15" flags="12905938976" default="0" value="0" />
<parameter name="均匀肤色" id="41" flags="12901679120" default="0" value="0" /> <parameter name="均匀肤色" id="41" flags="12905938976" default="0" value="0" />
<parameter name="牙齿美白" id="31" flags="12901679120" default="0" value="0" /> <parameter name="牙齿美白" id="31" flags="12905938976" default="0" value="0" />
<parameter name="眼睛提亮" id="32" flags="12901679120" default="0" value="0" /> <parameter name="眼睛提亮" id="32" flags="12905938976" default="0" value="0" />
</parameter> </parameter>
<parameter name="细节" id="102" flags="8589938704"> <parameter name="细节" id="102" flags="8589938704">
<parameter name="启用细节" id="73" flags="12901679120" default="1" value="1" /> <parameter name="启用细节" id="73" flags="12905938976" default="1" value="1" />
<parameter name="高频细节平滑" id="17" flags="12901679120" default="0" value="0" /> <parameter name="高频细节平滑" id="17" flags="12905938976" default="0" value="0" />
<parameter name="锐化" id="4" flags="12901679120" default="0" value="0" /> <parameter name="锐化" id="4" flags="12905938976" default="0" value="0" />
</parameter> </parameter>
<parameter name="脸型" id="103" flags="8589938704"> <parameter name="脸型" id="103" flags="8589938704">
<parameter name="启用脸型" id="72" flags="12901679120" default="1" value="1" /> <parameter name="启用脸型" id="72" flags="12905938976" default="1" value="1" />
<parameter name="瘦脸" id="18" flags="12901679120" default="0" value="0" /> <parameter name="瘦脸" id="18" flags="12905938976" default="0" value="0" />
<parameter name="大眼" id="33" flags="12901679120" default="0" value="0" /> <parameter name="大眼" id="33" flags="12905938976" default="0" value="0" />
<parameter name="短脸" id="22" flags="12901679120" default="0" value="0" /> <parameter name="短脸" id="22" flags="12905938976" default="0" value="0" />
<parameter name="窄脸" id="23" flags="12901679120" default="0" value="0" /> <parameter name="窄脸" id="23" flags="12905938976" default="0" value="0" />
<parameter name="下巴" id="24" flags="12901679120" default="0" value="0" /> <parameter name="下巴" id="24" flags="12905938976" default="0" value="0" />
<parameter name="V 脸" id="25" flags="12901679120" default="0" value="0" /> <parameter name="V 脸" id="25" flags="12905938976" default="0" value="0" />
<parameter name="下颌" id="26" flags="12901679120" default="0" value="0" /> <parameter name="下颌" id="26" flags="12905938976" default="0" value="0" />
<parameter name="颧骨" id="27" flags="12901679120" default="0" value="0" /> <parameter name="颧骨" id="27" flags="12905938976" default="0" value="0" />
<parameter name="小头" id="28" flags="12901679120" default="0" value="0" /> <parameter name="小头" id="28" flags="12905938976" default="0" value="0" />
<parameter name="小头范围" id="29" flags="12901679120" default="0" value="0" /> <parameter name="小头范围" id="29" flags="12905938976" default="0" value="0" />
</parameter> </parameter>
<parameter name="人脸选择与跟踪" id="104" flags="8589938704"> <parameter name="人脸选择与跟踪" id="104" flags="8589938704">
<parameter name="只处理选中人脸" id="20" flags="12901679120" default="0" value="0" /> <parameter name="只处理选中人脸" id="20" flags="12905938976" default="0" value="0" />
<parameter name="选脸位置" id="21" flags="12901683200"> <parameter name="选脸位置" id="21" flags="12905943072">
<foldFlags>15</foldFlags> <foldFlags>15</foldFlags>
<parameter name="X" id="1" flags="12901679120" default="0.5" value="0.5" /> <parameter name="X" id="1" flags="12905938976" default="0.5" value="0.5" />
<parameter name="Y" id="2" flags="12901679120" default="0.5" value="0.5" /> <parameter name="Y" id="2" flags="12905938976" default="0.5" value="0.5" />
</parameter> </parameter>
<parameter name="多人高速模式" id="39" flags="12901679120" default="0" value="0" /> <parameter name="多人高速模式" id="39" flags="12905938976" default="0" value="0" />
<parameter name="防跳脸范围" id="40" flags="12901679120" default="1.8" value="1.8" /> <parameter name="防跳脸范围" id="40" flags="12905938976" default="1.8" value="1.8" />
<parameter name="使用跟踪数据" id="45" flags="12901679120" default="0" value="0" /> <parameter name="使用跟踪数据" id="45" flags="12905938976" default="0" value="0" />
<parameter name="跟踪目标" id="46" flags="12901744656" default="0" value="0" /> <parameter name="跟踪目标" id="46" flags="12901744656" default="0" value="0" />
<parameter name="向前分析" id="50" /> <parameter name="向前分析" id="50" />
<parameter name="向后分析" id="51" /> <parameter name="向后分析" id="51" />
<parameter name="清除当前目标" id="52" /> <parameter name="清除当前目标" id="52" />
<parameter name="跟踪状态" id="53" flags="12889161760"> <parameter name="跟踪状态" id="53" flags="12905938976">
<text>选择画面中的人脸后开始分析</text> <text>选择画面中的人脸后开始分析</text>
</parameter> </parameter>
<parameter name="显示人脸框" id="19" flags="12901679120" default="0" value="0" /> <parameter name="显示人脸框" id="19" flags="12905938976" default="0" value="0" />
<parameter name="显示跟踪框" id="35" flags="12901679120" default="0" value="0" /> <parameter name="显示跟踪框" id="35" flags="12905938976" default="0" value="0" />
</parameter> </parameter>
<parameter name="蒙版与输出" id="105" flags="8589938704"> <parameter name="蒙版与输出" id="105" flags="8589938704">
<parameter name="蒙版阈值" id="7" flags="12901679120" default="148" value="148" /> <parameter name="蒙版阈值" id="7" flags="12905938976" default="148" value="148" />
<parameter name="蒙版柔度" id="8" flags="12901679120" default="0.6" value="0.6" /> <parameter name="蒙版柔度" id="8" flags="12905938976" default="0.6" value="0.6" />
<parameter name="边缘保护" id="9" flags="12901679120" default="0.4" value="0.4" /> <parameter name="边缘保护" id="9" flags="12905938976" default="0.4" value="0.4" />
<parameter name="预览蒙版" id="10" flags="12901679120" default="0" value="0" /> <parameter name="预览蒙版" id="10" flags="12905938976" default="0" value="0" />
<parameter name="反转蒙版" id="43" flags="12901679120" default="0" value="0" /> <parameter name="反转蒙版" id="43" flags="12905938976" default="0" value="0" />
<parameter name="蒙版通道" id="44" flags="12901744656" default="0" value="0" /> <parameter name="蒙版通道" id="44" flags="12901744656" default="0" value="0" />
<parameter name="整体混合" id="5" flags="12901679120" default="1" value="1" /> <parameter name="整体混合" id="5" flags="12905938976" default="1" value="1" />
</parameter> </parameter>
<parameter name="分析数据" id="60" flags="12905938976"> <parameter name="分析数据" id="60" flags="12905938976">
<text /> <text />
@@ -377,7 +341,7 @@
<parameter name="混合" id="10001" flags="12884901888" default="1" value="1" /> <parameter name="混合" id="10001" flags="12884901888" default="1" value="1" />
<parameter name="Flip" id="10002" flags="12889161760" default="0" value="0" /> <parameter name="Flip" id="10002" flags="12889161760" default="0" value="0" />
<parameter name="Input Points" id="10003" flags="12889161760" default="1" value="1" /> <parameter name="Input Points" id="10003" flags="12889161760" default="1" value="1" />
<parameter name="发布 OSC" id="10005" flags="12901679104" default="0" value="1" /> <parameter name="发布 OSC" id="10005" flags="12901679104" default="0" value="0" />
</filter> </filter>
</scenenode> </scenenode>
<aspectRatio>1</aspectRatio> <aspectRatio>1</aspectRatio>
+26 -18
查看文件
@@ -63,11 +63,13 @@
"/Localized: Plugin/InfoPlist.strings", "/Localized: Plugin/InfoPlist.strings",
"/Localized: Plugin/Localizable.strings", "/Localized: Plugin/Localizable.strings",
Plugin/beatygrilPlugIn.m, Plugin/beatygrilPlugIn.m,
Plugin/Info.plist,
Plugin/main.m,
Plugin/PGBeautyRenderer.m, Plugin/PGBeautyRenderer.m,
Plugin/PGFaceProcessor.m, Plugin/PGFaceProcessor.m,
Plugin/PGFaceSelectOSC.m, Plugin/PGFaceSelectOSC.m,
Plugin/Info.plist, Plugin/PGInspectorState.m,
Plugin/main.m, Plugin/PGInspectorView.m,
"Wrapper Application/Info.plist", "Wrapper Application/Info.plist",
); );
target = 6945FC8A3088B7220013E108 /* Wrapper Application */; target = 6945FC8A3088B7220013E108 /* Wrapper Application */;
@@ -306,11 +308,11 @@
6945FCCD3088B7220013E108 /* Debug configuration for PBXNativeTarget "XPC Service" */ = { 6945FCCD3088B7220013E108 /* Debug configuration for PBXNativeTarget "XPC Service" */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Manual;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_IDENTITY = "Developer ID Application";
ENABLE_HARDENED_RUNTIME = YES; CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = B3W25FX5H5; DEVELOPMENT_TEAM = B3W25FX5H5;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES;
INFOPLIST_FILE = beatygril/Plugin/Info.plist; INFOPLIST_FILE = beatygril/Plugin/Info.plist;
@@ -329,11 +331,11 @@
6945FCCE3088B7220013E108 /* Release configuration for PBXNativeTarget "XPC Service" */ = { 6945FCCE3088B7220013E108 /* Release configuration for PBXNativeTarget "XPC Service" */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Manual;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_IDENTITY = "Developer ID Application";
ENABLE_HARDENED_RUNTIME = YES; CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = B3W25FX5H5; DEVELOPMENT_TEAM = B3W25FX5H5;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES;
INFOPLIST_FILE = beatygril/Plugin/Info.plist; INFOPLIST_FILE = beatygril/Plugin/Info.plist;
@@ -353,11 +355,11 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements"; CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_IDENTITY = "Developer ID Application";
ENABLE_HARDENED_RUNTIME = YES; CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = B3W25FX5H5; DEVELOPMENT_TEAM = B3W25FX5H5;
ENABLE_HARDENED_RUNTIME = YES;
GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES;
INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist"; INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist";
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril; PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril;
@@ -370,11 +372,11 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements"; CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_IDENTITY = "Developer ID Application";
ENABLE_HARDENED_RUNTIME = YES; CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = B3W25FX5H5; DEVELOPMENT_TEAM = B3W25FX5H5;
ENABLE_HARDENED_RUNTIME = YES;
GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES;
INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist"; INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist";
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril; PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril;
@@ -387,9 +389,11 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk; ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk;
ARCHS = "arm64 x86_64";
ONLY_ACTIVE_ARCH = NO;
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = (
arm64,
x86_64,
);
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@@ -448,6 +452,7 @@
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = NO;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_VERSION = 5; SWIFT_VERSION = 5;
}; };
@@ -457,9 +462,11 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk; ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk;
ARCHS = "arm64 x86_64";
ONLY_ACTIVE_ARCH = NO;
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = (
arm64,
x86_64,
);
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@@ -512,6 +519,7 @@
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = NO;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_VERSION = 5; SWIFT_VERSION = 5;
}; };
+4 -4
查看文件
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>XPC!</string> <string>XPC!</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.4</string> <string>1.1.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>6</string> <string>9</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string></string> <string></string>
<key>PlugInKit</key> <key>PlugInKit</key>
@@ -72,7 +72,7 @@
<key>uuid</key> <key>uuid</key>
<string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string> <string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string>
<key>version</key> <key>version</key>
<string>1.0.4</string> <string>1.1.0</string>
</dict> </dict>
<dict> <dict>
<key>className</key> <key>className</key>
@@ -84,7 +84,7 @@
<key>uuid</key> <key>uuid</key>
<string>E0E432C8-3D71-4ABA-B006-E7F519E06C87</string> <string>E0E432C8-3D71-4ABA-B006-E7F519E06C87</string>
<key>version</key> <key>version</key>
<string>1.0.4</string> <string>1.1.0</string>
<key>protocolNames</key> <key>protocolNames</key>
<array> <array>
<string>FxOnScreenControl</string> <string>FxOnScreenControl</string>
+132 -16
查看文件
@@ -6,6 +6,14 @@
#include <float.h> #include <float.h>
static NSString * const PGFaceErrorDomain = @"media.dayu.PaintFCP.Face"; static NSString * const PGFaceErrorDomain = @"media.dayu.PaintFCP.Face";
// Contour origin / internal anchor, in the reference's recovered pin order.
// The unused middle entry of each reference triplet is intentionally omitted.
static const uint8_t PGThinPins[17][2] = {
{75,6},{59,13},{74,47},{60,47},{73,47},{61,47},
{72,48},{62,48},{71,48},{63,48},{70,49},{64,49},
{69,49},{65,49},{68,52},{66,52},{67,47}
};
static const uint8_t PGChinPins[3][2] = {{68,38},{67,37},{66,36}};
@interface PGFaceFrame () @interface PGFaceFrame ()
@property (nonatomic, copy, readwrite) NSArray<VNFaceObservation *> *faces; @property (nonatomic, copy, readwrite) NSArray<VNFaceObservation *> *faces;
@@ -63,14 +71,42 @@ static NSArray *PGRegion(VNFaceLandmarkRegion2D *region, CGRect box) {
} }
return points; return points;
} }
// This optional field has a fixed topology. Never remove an invalid point and
// shift the remaining indices: the local pins refer to exact Vision 76 indices.
static NSArray *PGSanitizedPoints76(id value) {
if (![value isKindOfClass:NSArray.class] || [value count] != 76) return nil;
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
for (id point in value) {
if (![point isKindOfClass:NSArray.class] || [point count] != 2) return nil;
for (id coordinate in point) {
if (![coordinate isKindOfClass:NSNumber.class] || !isfinite([coordinate doubleValue]) || fabs([coordinate doubleValue]) > 2) return nil;
}
[points addObject:[point copy]];
}
return [points copy];
}
static NSArray *PGFullPoints76(VNFaceLandmarkRegion2D *region, CGRect box) {
if (region.pointCount != 76 || !region.normalizedPoints) return nil;
const CGPoint *raw = region.normalizedPoints;
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
for (NSUInteger i=0; i<76; i++) {
if (!isfinite(raw[i].x) || !isfinite(raw[i].y)) return nil;
[points addObject:@[@(box.origin.x+raw[i].x*box.size.width),
@(box.origin.y+raw[i].y*box.size.height)]];
}
return PGSanitizedPoints76(points);
}
static NSDictionary *PGGeometry(VNFaceObservation *observation) { static NSDictionary *PGGeometry(VNFaceObservation *observation) {
CGRect box = observation.boundingBox; CGRect box = observation.boundingBox;
VNFaceLandmarks2D *lm = observation.landmarks; VNFaceLandmarks2D *lm = observation.landmarks;
return @{@"box": PGRectArray(box), NSMutableDictionary *geometry = [@{@"box": PGRectArray(box),
@"leftEye": PGRegion(lm.leftEye, box), @"rightEye": PGRegion(lm.rightEye, box), @"leftEye": PGRegion(lm.leftEye, box), @"rightEye": PGRegion(lm.rightEye, box),
@"leftBrow": PGRegion(lm.leftEyebrow, box), @"rightBrow": PGRegion(lm.rightEyebrow, box), @"leftBrow": PGRegion(lm.leftEyebrow, box), @"rightBrow": PGRegion(lm.rightEyebrow, box),
@"nose": PGRegion(lm.nose, box), @"lips": PGRegion(lm.outerLips, box), @"nose": PGRegion(lm.nose, box), @"lips": PGRegion(lm.outerLips, box),
@"mouth": PGRegion(lm.innerLips, box), @"contour": PGRegion(lm.faceContour, box)}; @"mouth": PGRegion(lm.innerLips, box), @"contour": PGRegion(lm.faceContour, box)} mutableCopy];
NSArray *points = PGFullPoints76(lm.allPoints, box);
if (points) geometry[@"points76"] = points;
return [geometry copy];
} }
static NSDictionary *PGSanitizedGeometry(id value) { static NSDictionary *PGSanitizedGeometry(id value) {
if (![value isKindOfClass:NSDictionary.class]) return nil; if (![value isKindOfClass:NSDictionary.class]) return nil;
@@ -88,6 +124,10 @@ static NSDictionary *PGSanitizedGeometry(id value) {
} }
result[name] = points; result[name] = points;
} }
// Old archives and partially detected faces remain usable by the other
// controls. Missing or malformed full topology disables local thin/chin pins.
NSArray *points76 = PGSanitizedPoints76(value[@"points76"]);
if (points76) result[@"points76"] = points76;
return [result copy]; return [result copy];
} }
static CGRect PGRegionBounds(NSDictionary *geometry, NSString *name, CGRect fallback) { static CGRect PGRegionBounds(NSDictionary *geometry, NSString *name, CGRect fallback) {
@@ -133,7 +173,7 @@ static CGPoint PGRemapPoint(NSArray *point, CGRect old, CGRect current) {
return CGPointMake(current.origin.x+([point[0] doubleValue]-old.origin.x)*current.size.width/old.size.width, return CGPointMake(current.origin.x+([point[0] doubleValue]-old.origin.x)*current.size.width/old.size.width,
current.origin.y+([point[1] doubleValue]-old.origin.y)*current.size.height/old.size.height); current.origin.y+([point[1] doubleValue]-old.origin.y)*current.size.height/old.size.height);
} }
static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previous, double dt) { static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previous, double dt, BOOL stabilizePoints76) {
CGRect old=PGArrayRect(previous[@"box"]),box=PGArrayRect(current[@"box"]); CGRect old=PGArrayRect(previous[@"box"]),box=PGArrayRect(current[@"box"]);
if(!PGValidRect(old) || !PGValidRect(box))return current; if(!PGValidRect(old) || !PGValidRect(box))return current;
// Independent stabilization: translate/scale old landmarks into the current // Independent stabilization: translate/scale old landmarks into the current
@@ -160,6 +200,26 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
} }
stable[name]=[points copy]; stable[name]=[points copy];
} }
NSArray *current76 = PGSanitizedPoints76(current[@"points76"]);
NSArray *previous76 = PGSanitizedPoints76(previous[@"points76"]);
if (current76) {
if (stabilizePoints76 && previous76) {
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
for (NSUInteger i=0; i<76; i++) {
CGPoint p = PGRemapPoint(previous76[i], old, box);
if (!axisHold) {
p.x += ([current76[i][0] doubleValue]-p.x)*weight;
p.y += ([current76[i][1] doubleValue]-p.y)*weight;
}
[points addObject:@[@(p.x),@(p.y)]];
}
stable[@"points76"] = [points copy];
} else {
// Reacquired detections and changes from/to incomplete topology do
// not borrow a held face's old projected pose or invent missing points.
stable[@"points76"] = current76;
}
}
return [stable copy]; return [stable copy];
} }
@@ -240,7 +300,7 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
else if(index>=0) { else if(index>=0) {
NSDictionary *current=raw[(NSUInteger)index]; NSDictionary *current=raw[(NSUInteger)index];
double dt=reference?CMTimeGetSeconds(CMTimeSubtract(time,PGRecordTime(reference))):0; double dt=reference?CMTimeGetSeconds(CMTimeSubtract(time,PGRecordTime(reference))):0;
output=@[previous?PGStableGeometry(current,previous,dt):current]; output=@[previous?PGStableGeometry(current,previous,dt,![reference[@"held"] boolValue]):current];
} else {output=prior?:@[];held=output.count>0;} } else {output=prior?:@[];held=output.count>0;}
NSMutableDictionary *record=[@{@"context":context,@"scope":scope,@"key":key?:@"invalid",@"value":@(time.value),@"scale":@(time.timescale),@"epoch":@(time.epoch), NSMutableDictionary *record=[@{@"context":context,@"scope":scope,@"key":key?:@"invalid",@"value":@(time.value),@"scale":@(time.timescale),@"epoch":@(time.epoch),
@"seedX":@(seedX),@"seedY":@(seedY),@"raw":raw,@"output":output,@"index":@(index),@"held":@(held),@"referenceKey":reference[@"key"]?:@""} mutableCopy]; @"seedX":@(seedX),@"seedY":@(seedY),@"raw":raw,@"output":output,@"index":@(index),@"held":@(held),@"referenceKey":reference[@"key"]?:@""} mutableCopy];
@@ -284,11 +344,25 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
_lock = [[NSLock alloc] init]; _lock = [[NSLock alloc] init];
_realtime = [PGRealtimeFaceTracker new]; _realtime = [PGRealtimeFaceTracker new];
_targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil]; _targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil];
// Analytic, feathered sampling deformation authored for this project. // Local thinning and chin use the recovered 17/3-pin coordinate math.
// Positive short-face shortens the lower face; positive chin lengthens it. // The other seven controls retain this project's analytic approximation.
// This is an independent approximation, not the reference vendor's warp. // Chin follows those analytic body controls and precedes the eye warp;
NSString *source = @"kernel vec2 pgFaceWarp(vec2 c, vec2 size, vec2 le, vec2 re, vec2 er, float thin, float bigEye, float shortFace, float narrow, float chin, float vFace, float jaw, float cheek, float smallHead, float headRange) {" // this combined order is not a reconstruction of the full reference chain.
" vec2 p = destCoord(); vec2 n = (p-c)/size;" // Each pin changes the sampling coordinate, with only one image resample.
NSMutableString *source = [NSMutableString stringWithString:
@"vec2 pgThinPin(vec2 q, vec4 control, float ratio) {"
" vec2 direction=control.zw-control.xy; float radius=length(direction);"
" if(radius<=0.0001) return q;"
" float weight=clamp(1.0-length(q-control.xy)/radius,0.0,1.0);"
" return clamp(q-direction*ratio*weight,vec2(0.0),vec2(1.0)); }"
" kernel vec2 pgFaceWarp(vec2 c, vec2 size, vec2 le, vec2 re, vec2 er, float thinRatio, float bigEye, float shortFace, float narrow, float chinRatio, float vFace, float jaw, float cheek, float smallHead, float headRange, vec2 imageOrigin, vec2 imageSize"];
for (NSUInteger i=0; i<17; i++) [source appendFormat:@", vec4 pin%lu",(unsigned long)i];
for (NSUInteger i=0; i<3; i++) [source appendFormat:@", vec4 chinPin%lu",(unsigned long)i];
[source appendString:@") { vec2 p=destCoord();"
" if(thinRatio>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"];
for (NSUInteger i=0; i<17; i++) [source appendFormat:@" q=pgThinPin(q,pin%lu,thinRatio);",(unsigned long)i];
[source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }"
" vec2 n = (p-c)/size;"
" float body = 1.0-smoothstep(0.38,0.76,length(n*vec2(1.0,0.82)));" " float body = 1.0-smoothstep(0.38,0.76,length(n*vec2(1.0,0.82)));"
" float lower = (1.0-smoothstep(-0.12,0.27,n.y))*body;" " float lower = (1.0-smoothstep(-0.12,0.27,n.y))*body;"
" float sides = smoothstep(0.08,0.32,abs(n.x));" " float sides = smoothstep(0.08,0.32,abs(n.x));"
@@ -296,12 +370,15 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
" float ch=(n.y-0.02)/0.19; float cheekBand = exp(-ch*ch)*body;" " float ch=(n.y-0.02)/0.19; float cheekBand = exp(-ch*ch)*body;"
" float head = 1.0-smoothstep(0.38,0.75+0.32*headRange,length(n*vec2(0.9,0.65)));" " float head = 1.0-smoothstep(0.38,0.75+0.32*headRange,length(n*vec2(0.9,0.65)));"
" p += (p-c)*smallHead*0.18*head;" " p += (p-c)*smallHead*0.18*head;"
" p.x += size.x*n.x*(narrow*0.30*body + thin*0.26*lower + vFace*0.24*jawBand + jaw*0.20*jawBand + cheek*0.18*cheekBand)*sides;" " p.x += size.x*n.x*(narrow*0.30*body + vFace*0.24*jawBand + jaw*0.20*jawBand + cheek*0.18*cheekBand)*sides;"
" float cn=n.x/0.30; p.y += size.y*(-shortFace*0.16*lower + chin*0.13*exp(-cn*cn)*jawBand - vFace*0.025*lower);" " p.y += size.y*(-shortFace*0.16*lower - vFace*0.025*lower);"
" if(abs(chinRatio)>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"];
for (NSUInteger i=0; i<3; i++) [source appendFormat:@" q=pgThinPin(q,chinPin%lu,chinRatio);",(unsigned long)i];
[source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }"
" vec2 dl = (p-le)/er; float wl = 1.0-smoothstep(0.0,1.0,length(dl));" " vec2 dl = (p-le)/er; float wl = 1.0-smoothstep(0.0,1.0,length(dl));"
" p -= (p-le)*bigEye*0.33*wl*wl;" " p -= (p-le)*bigEye*0.33*wl*wl;"
" vec2 dr = (p-re)/er; float wr = 1.0-smoothstep(0.0,1.0,length(dr));" " vec2 dr = (p-re)/er; float wr = 1.0-smoothstep(0.0,1.0,length(dr));"
" p -= (p-re)*bigEye*0.33*wr*wr; return p; }"; " p -= (p-re)*bigEye*0.33*wr*wr; return p; }"];
#pragma clang diagnostic push #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations" #pragma clang diagnostic ignored "-Wdeprecated-declarations"
_warp = [CIWarpKernel kernelWithString:source]; _warp = [CIWarpKernel kernelWithString:source];
@@ -320,6 +397,8 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
CIImage *prepared = [image imageByApplyingTransform:CGAffineTransformMakeTranslation(-extent.origin.x, -extent.origin.y)]; CIImage *prepared = [image imageByApplyingTransform:CGAffineTransformMakeTranslation(-extent.origin.x, -extent.origin.y)];
prepared = [prepared imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)]; prepared = [prepared imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
VNDetectFaceLandmarksRequest *request = [[VNDetectFaceLandmarksRequest alloc] init]; VNDetectFaceLandmarksRequest *request = [[VNDetectFaceLandmarksRequest alloc] init];
request.revision = VNDetectFaceLandmarksRequestRevision3;
request.constellation = VNRequestFaceLandmarksConstellation76Points;
// An image request handler is intentionally local to one invocation. A shared // An image request handler is intentionally local to one invocation. A shared
// sequence handler would make parallel / reverse render order observable. // sequence handler would make parallel / reverse render order observable.
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCIImage:prepared options:@{}]; VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCIImage:prepared options:@{}];
@@ -418,14 +497,51 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
CIImage *result = image; CIImage *result = image;
for (NSDictionary *face in geometry) { for (NSDictionary *face in geometry) {
CGRect box = PGArrayRect(face[@"box"]), pixels = PGToPixels(box, extent); CGRect box = PGArrayRect(face[@"box"]), pixels = PGToPixels(box, extent);
NSArray *points76 = PGSanitizedPoints76(face[@"points76"]);
float thinRatio = 0, chinRatio = 0;
if (points76) {
double width = hypot(([points76[75][0] doubleValue]-[points76[59][0] doubleValue])*extent.size.width,
([points76[75][1] doubleValue]-[points76[59][1] doubleValue])*extent.size.height);
double midX = ([points76[74][0] doubleValue]+[points76[60][0] doubleValue])*.5;
double midY = ([points76[74][1] doubleValue]+[points76[60][1] doubleValue])*.5;
double height = 2*hypot(([points76[67][0] doubleValue]-midX)*extent.size.width,
([points76[67][1] doubleValue]-midY)*extent.size.height);
double gain = fmin(2.25,fmax(1,280/fmax(1,fmax(width,height))));
thinRatio = PGClamp(s.faceThin,0,1)*.025*gain;
float chin = PGClamp(s.faceChin,-1,1);
chinRatio = chin*(chin>0 ? .04 : .02)*gain;
}
BOOL otherControls = s.faceBigEye || s.faceShort || s.faceNarrow || s.faceV || s.faceJaw || s.faceCheekbone || s.faceSmallHead;
// No full topology means no guessed bbox thin/chin deformation. Legacy
// archives using only those controls remain exact image/mask pass-through.
if (thinRatio <= .000001 && fabsf(chinRatio) <= .000001 && !otherControls) continue;
CGRect le = PGRegionBounds(face, @"leftEye", PGInFace(box, 0.16, 0.53, 0.25, 0.15)); CGRect le = PGRegionBounds(face, @"leftEye", PGInFace(box, 0.16, 0.53, 0.25, 0.15));
CGRect re = PGRegionBounds(face, @"rightEye", PGInFace(box, 0.59, 0.53, 0.25, 0.15)); CGRect re = PGRegionBounds(face, @"rightEye", PGInFace(box, 0.59, 0.53, 0.25, 0.15));
CGPoint left = PGCenter(PGToPixels(le, extent)), right = PGCenter(PGToPixels(re, extent)); CGPoint left = PGCenter(PGToPixels(le, extent)), right = PGCenter(PGToPixels(re, extent));
NSArray *arguments = @[PGVector(PGCenter(pixels)), [CIVector vectorWithX:pixels.size.width Y:pixels.size.height], NSMutableArray *arguments = [@[PGVector(PGCenter(pixels)), [CIVector vectorWithX:pixels.size.width Y:pixels.size.height],
PGVector(left), PGVector(right), [CIVector vectorWithX:MAX(2, pixels.size.width * 0.24) Y:MAX(2, pixels.size.height * 0.17)], PGVector(left), PGVector(right), [CIVector vectorWithX:MAX(2, pixels.size.width * 0.24) Y:MAX(2, pixels.size.height * 0.17)],
@(PGClamp(s.faceThin, 0, 1)), @(PGClamp(s.faceBigEye, 0, 1)), @(PGClamp(s.faceShort, -1, 1)), @(thinRatio), @(PGClamp(s.faceBigEye, 0, 1)), @(PGClamp(s.faceShort, -1, 1)),
@(PGClamp(s.faceNarrow, -1, 1)), @(PGClamp(s.faceChin, -1, 1)), @(PGClamp(s.faceV, 0, 1)), @(PGClamp(s.faceNarrow, -1, 1)), @(chinRatio), @(PGClamp(s.faceV, 0, 1)),
@(PGClamp(s.faceJaw, 0, 1)), @(PGClamp(s.faceCheekbone, 0, 1)), @(PGClamp(s.faceSmallHead, 0, 1)), @(PGClamp(s.faceSmallHeadRange, 0, 1))]; @(PGClamp(s.faceJaw, 0, 1)), @(PGClamp(s.faceCheekbone, 0, 1)), @(PGClamp(s.faceSmallHead, 0, 1)), @(PGClamp(s.faceSmallHeadRange, 0, 1)),
[CIVector vectorWithX:extent.origin.x Y:extent.origin.y], [CIVector vectorWithX:extent.size.width Y:extent.size.height]] mutableCopy];
for (NSUInteger i=0; i<17; i++) {
double ox=0,oy=0,ax=0,ay=0;
if (points76) {
NSArray *origin=points76[PGThinPins[i][0]],*anchor=points76[PGThinPins[i][1]];
ox=[origin[0] doubleValue];oy=1-[origin[1] doubleValue];
ax=[anchor[0] doubleValue];ay=1-[anchor[1] doubleValue];
}
[arguments addObject:[CIVector vectorWithX:ox Y:oy Z:ax W:ay]];
}
for (NSUInteger i=0; i<3; i++) {
double ox=0,oy=0,ax=0,ay=0;
if (points76) {
NSArray *origin=points76[PGChinPins[i][0]],*anchor=points76[PGChinPins[i][1]];
ox=[origin[0] doubleValue];oy=1-[origin[1] doubleValue];
ax=[anchor[0] doubleValue];ay=1-[anchor[1] doubleValue];
}
[arguments addObject:[CIVector vectorWithX:ox Y:oy Z:ax W:ay]];
}
CIImage *input = [result imageByClampingToExtent]; CIImage *input = [result imageByClampingToExtent];
result = [_warp applyWithExtent:extent roiCallback:^CGRect(int index, CGRect rect) { (void)index; (void)rect; return extent; } inputImage:input arguments:arguments]; result = [_warp applyWithExtent:extent roiCallback:^CGRect(int index, CGRect rect) { (void)index; (void)rect; return extent; } inputImage:input arguments:arguments];
if (!result) return nil; if (!result) return nil;
@@ -0,0 +1,43 @@
#import <Foundation/Foundation.h>
#import <FxPlug/FxPlugSDK.h>
#import "PGBeautyTypes.h"
#import "PGParameterIDs.h"
NS_ASSUME_NONNULL_BEGIN
/// The descriptor list is shared by UI, persistence and render settings.
/// Keys: id, name, section (smooth/color/shape/more), kind
/// (slider/toggle/preset/master), min, max, default, format, bypass.
FOUNDATION_EXPORT NSArray<NSDictionary *> *PGInspectorControls(void);
FOUNDATION_EXPORT NSDictionary * _Nullable PGInspectorDescriptor(UInt32 identifier);
@interface PGInspectorState : NSObject <NSSecureCoding, NSCopying, FxCustomParameterInterpolation_v2>
@property (nonatomic, copy, readonly) NSDictionary<NSNumber *, NSNumber *> *values;
@property (nonatomic, copy, readonly) NSSet<NSNumber *> *disabledParameters;
/// An untouched panel must not replace parameters from an existing document.
@property (nonatomic, readonly) BOOL hasEdits;
+ (instancetype)stateWithValues:(NSDictionary *)values disabledParameters:(NSSet *)disabled hasEdits:(BOOL)edited;
- (double)valueForParameter:(UInt32)identifier;
- (BOOL)isParameterEnabled:(UInt32)identifier;
@end
FOUNDATION_EXPORT PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings settings, PGInspectorState * _Nullable state);
/// All arbitrary-time FxPlug parameter access is bracketed by host actions.
/// The published custom object owns the full UI state, so FCP save/undo does
/// not depend on unpublished parameters being mirrored by the template.
@interface PGInspectorController : NSObject
@property (nonatomic, strong, readonly) PGInspectorState *state;
@property (nonatomic, readonly) BOOL available;
@property (nonatomic, copy, readonly) NSString *statusMessage;
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager;
- (void)refresh;
- (void)documentDidChange;
- (void)beginInteraction;
- (void)endInteraction;
- (BOOL)setValue:(double)value forParameter:(UInt32)identifier;
- (BOOL)toggleEnabledForParameter:(UInt32)identifier;
- (BOOL)resetParameter:(UInt32)identifier;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,245 @@
#import "PGInspectorState.h"
#import <math.h>
#import <stddef.h>
NSArray<NSDictionary *> *PGInspectorControls(void) {
static NSArray *controls; static dispatch_once_t once;
dispatch_once(&once, ^{
PGBeautySettings defaults=PGBeautyDefaults();
NSMutableArray *items=[NSMutableArray new];
#define ITEM(ID,NAME,FIELD,SECTION,KIND,FORMAT,MIN,MAX,BYPASS,STORAGE) \
[items addObject:@{@"id":@(ID),@"name":@NAME,@"section":@SECTION,@"kind":@KIND,@"format":@FORMAT,@"min":@(MIN),@"max":@(MAX),@"default":@(defaults.FIELD),@"bypass":@(BYPASS),@"offset":@(offsetof(PGBeautySettings,FIELD)),@"storage":@STORAGE}]
ITEM(PGSmoothingEnabled,"磨皮",featureProtection,"smooth","master","integer",0,1,NO,"master");
ITEM(PGMidtone,"局部磨皮",midtoneDetail,"smooth","slider","percent",0,1,YES,"float");
ITEM(PGHighPass,"全局磨皮",highPassDetail,"smooth","slider","percent",0,1,YES,"float");
ITEM(PGFeatureProtect,"五官保护",featureProtection,"smooth","toggle","integer",0,1,NO,"uint");
ITEM(PGColorEnabled,"美白",featureProtection,"color","master","integer",0,1,NO,"master");
ITEM(PGTonePreset,"肤色",skinTonePreset,"color","preset","integer",0,6,NO,"uint");
NSMutableDictionary *preset=[items.lastObject mutableCopy];
preset[@"options"]=@[@"原色",@"自然白",@"冷白",@"暖白",@"红润白",@"小麦",@"古铜"];items[items.count-1]=preset;
ITEM(PGBrightness,"肤色亮度",skinBrightness,"color","slider","percent",0,1,YES,"float");
ITEM(PGTeeth,"白牙",teethWhitening,"color","slider","percent",0,1,YES,"float");
ITEM(PGHue,"皮肤色相",skinHue,"color","slider","degrees",-30,30,YES,"float");
ITEM(PGShapeEnabled,"瘦脸",featureProtection,"shape","master","integer",0,1,NO,"master");
ITEM(PGTrackBoxes,"人脸框预览",showTrackBox,"shape","toggle","integer",0,1,NO,"uint");
ITEM(PGFastMulti,"多人模式",fastMultiFace,"shape","toggle","integer",0,1,NO,"uint");
ITEM(PGJumpGuard,"防跳脸范围",faceJumpGuardRange,"shape","slider","percent",1,4,NO,"float");
ITEM(PGThin,"瘦脸",faceThin,"shape","slider","signed",0,1,YES,"float");
ITEM(PGBigEye,"大眼",faceBigEye,"shape","slider","signed",0,1,YES,"float");
ITEM(PGShort,"短脸",faceShort,"shape","slider","signed",-1,1,YES,"float");
ITEM(PGNarrow,"窄脸",faceNarrow,"shape","slider","signed",-1,1,YES,"float");
ITEM(PGChin,"下巴",faceChin,"shape","slider","signed",-1,1,YES,"float");
ITEM(PGVFace,"V脸",faceV,"shape","slider","signed",0,1,YES,"float");
ITEM(PGJaw,"下颌骨",faceJaw,"shape","slider","signed",0,1,YES,"float");
ITEM(PGCheekbone,"颧骨",faceCheekbone,"shape","slider","signed",0,1,YES,"float");
ITEM(PGSmallHead,"小头",faceSmallHead,"shape","slider","signed",0,1,YES,"float");
ITEM(PGSmallHeadRange,"小头范围",faceSmallHeadRange,"shape","slider","percent",0,1,NO,"float");
ITEM(PGAmount,"磨皮强度",amount,"more","slider","percent",0,1,YES,"float");
ITEM(PGRadius,"磨皮半径",radius,"more","slider","radius",1,30,NO,"float");
ITEM(PGSaturation,"肤色饱和度",skinSaturation,"more","slider","percent",-100,100,YES,"float");
ITEM(PGEvenTone,"均匀肤色",skinEvenTone,"more","slider","percent",0,1,YES,"float");
ITEM(PGEyeBright,"眼睛提亮",eyeBrightening,"more","slider","percent",0,1,YES,"float");
ITEM(PGSharpness,"锐化",sharpness,"more","slider","percent",0,1,YES,"float");
ITEM(PGMix,"整体混合",mix,"more","slider","percent",0,1,NO,"float");
#undef ITEM
controls=[items copy];
}); return controls;
}
NSDictionary *PGInspectorDescriptor(UInt32 identifier) {
for(NSDictionary *d in PGInspectorControls()) if([d[@"id"] unsignedIntValue]==identifier) return d;
return nil;
}
static double PGControlValue(id raw,NSDictionary *d) {
double value=[raw isKindOfClass:NSNumber.class]?[raw doubleValue]:[d[@"default"] doubleValue];
if(!isfinite(value)) value=[d[@"default"] doubleValue];
value=fmin([d[@"max"] doubleValue],fmax([d[@"min"] doubleValue],value));
if(![d[@"storage"] isEqual:@"float"]) value=round(value);
return value;
}
@implementation PGInspectorState
+ (BOOL)supportsSecureCoding {return YES;}
+ (instancetype)stateWithValues:(NSDictionary *)values disabledParameters:(NSSet *)disabled hasEdits:(BOOL)edited {
PGInspectorState *state=[self new]; NSMutableDictionary *safe=[NSMutableDictionary new];NSMutableSet *bypassed=[NSMutableSet new];
for(NSDictionary *d in PGInspectorControls()) {
NSNumber *key=d[@"id"];safe[key]=@(PGControlValue(values[key],d));
if([d[@"bypass"] boolValue] && [disabled containsObject:key]) [bypassed addObject:key];
}
state->_values=[safe copy];state->_disabledParameters=[bypassed copy];state->_hasEdits=edited;return state;
}
- (instancetype)init {if((self=[super init])) {_values=@{};_disabledParameters=[NSSet set];}return self;}
- (instancetype)initWithCoder:(NSCoder *)coder {
NSInteger version=[coder decodeIntegerForKey:@"schema"];
if(version!=1) return nil;
NSSet *classes=[NSSet setWithObjects:NSDictionary.class,NSNumber.class,nil];
NSDictionary *values=[coder decodeObjectOfClasses:classes forKey:@"values"];
NSSet *disabled=[coder decodeObjectOfClasses:[NSSet setWithObjects:NSSet.class,NSNumber.class,nil] forKey:@"disabled"];
return [PGInspectorState stateWithValues:values?:@{} disabledParameters:disabled?:[NSSet set] hasEdits:[coder decodeBoolForKey:@"edited"]];
}
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeInteger:1 forKey:@"schema"];[coder encodeObject:_values forKey:@"values"];
[coder encodeObject:_disabledParameters forKey:@"disabled"];[coder encodeBool:_hasEdits forKey:@"edited"];
}
- (id)copyWithZone:(NSZone *)zone {return [PGInspectorState stateWithValues:_values disabledParameters:_disabledParameters hasEdits:_hasEdits];}
- (double)valueForParameter:(UInt32)identifier {NSDictionary *d=PGInspectorDescriptor(identifier);return d?PGControlValue(_values[@(identifier)],d):0;}
- (BOOL)isParameterEnabled:(UInt32)identifier {return ![_disabledParameters containsObject:@(identifier)];}
- (BOOL)isEqual:(id)object {
if(object==self)return YES;
if(![object isKindOfClass:PGInspectorState.class])return NO;
PGInspectorState *other=object;return _hasEdits==other.hasEdits && [_values isEqual:other.values] && [_disabledParameters isEqual:other.disabledParameters];
}
- (NSUInteger)hash {return _values.hash ^ _disabledParameters.hash ^ _hasEdits;}
- (NSObject<NSSecureCoding,NSCopying> *)interpolateBetween:(NSObject<NSSecureCoding,NSCopying> *)rightValue withWeight:(float)weight {
if(![rightValue isKindOfClass:PGInspectorState.class])return [self copy];
PGInspectorState *right=(PGInspectorState *)rightValue;double t=isfinite(weight)?fmin(1,fmax(0,weight)):0;
if(t==0)return [self copy];if(t==1)return [right copy];
if(!_hasEdits || !right.hasEdits)return [self copy];
NSMutableDictionary *values=[NSMutableDictionary new];
for(NSDictionary *d in PGInspectorControls()) {
UInt32 key=[d[@"id"] unsignedIntValue];double a=[self valueForParameter:key],b=[right valueForParameter:key];
// Menus, switches and enable masks hold the preceding key until the
// next key. Numeric slider values interpolate within their valid range.
values[@(key)]=[d[@"storage"] isEqual:@"float"]?@(a+(b-a)*t):@(a);
}
return [PGInspectorState stateWithValues:values disabledParameters:_disabledParameters hasEdits:_hasEdits||right.hasEdits];
}
@end
PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorState *state) {
if(!state.hasEdits)return s;
for(NSDictionary *d in PGInspectorControls()) {
NSString *storage=d[@"storage"];if([storage isEqual:@"master"])continue;
UInt32 key=[d[@"id"] unsignedIntValue];double value=[state valueForParameter:key];
if(![state isParameterEnabled:key]) value=0;
size_t offset=[d[@"offset"] unsignedIntegerValue];
if([storage isEqual:@"float"]) *(float *)((uint8_t *)&s+offset)=value;
else *(uint32_t *)((uint8_t *)&s+offset)=(uint32_t)value;
}
if(![state valueForParameter:PGSmoothingEnabled])s.amount=s.midtoneDetail=s.highPassDetail=0;
if(![state valueForParameter:PGColorEnabled]) {s.skinBrightness=s.skinHue=s.skinSaturation=s.skinEvenTone=s.teethWhitening=s.eyeBrightening=0;s.skinTonePreset=0;}
if(![state valueForParameter:PGShapeEnabled]) {s.faceThin=s.faceBigEye=s.faceShort=s.faceNarrow=s.faceChin=s.faceV=s.faceJaw=s.faceCheekbone=s.faceSmallHead=0;s.showTrackBox=0;}
return s;
}
@interface PGInspectorController ()
@property (nonatomic,strong,readwrite) PGInspectorState *state;
@property (nonatomic,readwrite) BOOL available;
@property (nonatomic,copy,readwrite) NSString *statusMessage;
@property (nonatomic,strong) id<PROAPIAccessing> manager;
@property (nonatomic,strong) id<FxCustomParameterActionAPI_v4> action;
@property (nonatomic) BOOL interacting;
@property (nonatomic) NSTimeInterval pendingUntil;
@property (nonatomic) CMTime pendingTime;
@end
@implementation PGInspectorController
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager {
if((self=[super init])){_manager=manager;_state=[PGInspectorState stateWithValues:@{} disabledParameters:[NSSet set] hasEdits:NO];_statusMessage=@"";}
return self;
}
- (BOOL)start {
if(_interacting)return YES;
_action=[_manager apiForProtocol:@protocol(FxCustomParameterActionAPI_v4)];
id retrieval=[_manager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
id setting=[_manager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
_available=_action && retrieval && setting;
if(!_available){_statusMessage=@"等待宿主连接";return NO;}
[_action startAction:self];_interacting=YES;return YES;
}
- (void)finish {if(_interacting){_interacting=NO;[_action endAction:self];_action=nil;}}
- (void)readAtTime:(CMTime)time {
if(_pendingUntil && CMTimeCompare(time,_pendingTime)!=0)_pendingUntil=0;
id<FxParameterRetrievalAPI_v6> api=[_manager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
NSObject *object=nil;[api getCustomParameterValue:&object fromParameter:PGInspectorPanel atTime:time];
if([object isKindOfClass:PGInspectorState.class] && [(PGInspectorState *)object hasEdits]) {
PGInspectorState *host=(PGInspectorState *)object;
if([host isEqual:_state])_pendingUntil=0;
if(!_pendingUntil || NSDate.timeIntervalSinceReferenceDate>=_pendingUntil)_state=[host copy];
return;
}
if(_pendingUntil && NSDate.timeIntervalSinceReferenceDate<_pendingUntil)return;
NSMutableDictionary *values=[NSMutableDictionary new];
for(NSDictionary *d in PGInspectorControls()) {
UInt32 key=[d[@"id"] unsignedIntValue];double value=[d[@"default"] doubleValue];
if([d[@"kind"] isEqual:@"preset"]) {int menu=(int)value;if([api getIntValue:&menu fromParameter:key atTime:time])value=menu;}
else if([d[@"kind"] isEqual:@"toggle"] || [d[@"kind"] isEqual:@"master"]) {BOOL on=value!=0;if([api getBoolValue:&on fromParameter:key atTime:time])value=on;}
else [api getFloatValue:&value fromParameter:key atTime:time];
values[@(key)]=@(value);
}
BOOL legacyDetail=YES;[api getBoolValue:&legacyDetail fromParameter:PGDetailEnabled atTime:time];
NSMutableSet *disabled=legacyDetail?[NSMutableSet set]:[NSMutableSet setWithObjects:@(PGHighPass),@(PGSharpness),nil];
// Earlier versions put high-pass smoothing under a different group. Keep
// its effective state when importing a document whose local-skin group was
// off, rather than turning a still-active global effect off on first edit.
if(![values[@(PGSmoothingEnabled)] boolValue] && legacyDetail && [values[@(PGHighPass)] doubleValue]>0) {
values[@(PGSmoothingEnabled)]=@YES;
[disabled addObject:@(PGMidtone)];[disabled addObject:@(PGAmount)];
}
_state=[PGInspectorState stateWithValues:values disabledParameters:disabled hasEdits:NO];
}
- (void)refresh {
if(_interacting)return;
if(![self start])return;
@try {[self readAtTime:[_action currentTime]];_statusMessage=@"";} @finally {[self finish];}
}
- (void)documentDidChange {if(_interacting)return;_pendingUntil=0;[self refresh];}
- (void)beginInteraction {
if(_interacting)return;
if([self start]) {
// A new gesture starts from the authoritative document, including an
// immediate undo. The short pending hold only prevents display flicker;
// it must never resurrect an undone value in a subsequent edit.
_pendingUntil=0;[self readAtTime:[_action currentTime]];
}
}
- (void)endInteraction {[self finish];}
- (BOOL)commit:(PGInspectorState *)next {
if([next isEqual:_state] && _state.hasEdits)return YES;
id<FxParameterSettingAPI_v5> api=[_manager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
CMTime time=[_action currentTime];
// The published object is authoritative. Only mirror after its write was
// accepted; a rejected write must never leave half-applied hidden values.
if(![api setCustomParameterValue:next toParameter:PGInspectorPanel atTime:time]){_statusMessage=@"参数未能保存,请重试";return NO;}
for(NSDictionary *d in PGInspectorControls()) {
UInt32 key=[d[@"id"] unsignedIntValue];double value=[next valueForParameter:key];
if(![next isParameterEnabled:key])value=0;
double previous=[_state isParameterEnabled:key]?[_state valueForParameter:key]:0;
if(previous==value)continue;
if([d[@"kind"] isEqual:@"preset"]) [api setIntValue:(int)value toParameter:key atTime:time];
else if([d[@"kind"] isEqual:@"toggle"] || [d[@"kind"] isEqual:@"master"]) [api setBoolValue:value!=0 toParameter:key atTime:time];
else [api setFloatValue:value toParameter:key atTime:time];
}
_state=next;_pendingUntil=NSDate.timeIntervalSinceReferenceDate+.35;_pendingTime=time;_statusMessage=@"";return YES;
}
- (BOOL)setValue:(double)value forParameter:(UInt32)identifier {
NSDictionary *d=PGInspectorDescriptor(identifier);if(!d || !isfinite(value))return NO;
BOOL owned=!_interacting;if(owned)[self beginInteraction];if(!_interacting)return NO;
BOOL ok=NO;@try {
NSMutableDictionary *values=[_state.values mutableCopy];values[@(identifier)]=@(PGControlValue(@(value),d));
ok=[self commit:[PGInspectorState stateWithValues:values disabledParameters:_state.disabledParameters hasEdits:YES]];
} @finally {if(owned)[self finish];}return ok;
}
- (BOOL)toggleEnabledForParameter:(UInt32)identifier {
NSDictionary *d=PGInspectorDescriptor(identifier);if(!d)return NO;
if([d[@"kind"] isEqual:@"master"] || [d[@"kind"] isEqual:@"toggle"]) {
BOOL owned=!_interacting;if(owned)[self beginInteraction];if(!_interacting)return NO;
BOOL ok=NO;@try {ok=[self setValue:![_state valueForParameter:identifier] forParameter:identifier];}
@finally {if(owned)[self finish];}return ok;
}
if(![d[@"bypass"] boolValue])return NO;
BOOL owned=!_interacting;if(owned)[self beginInteraction];if(!_interacting)return NO;
BOOL ok=NO;@try {
NSMutableSet *disabled=[_state.disabledParameters mutableCopy];NSNumber *key=@(identifier);
if([disabled containsObject:key])[disabled removeObject:key];else [disabled addObject:key];
ok=[self commit:[PGInspectorState stateWithValues:_state.values disabledParameters:disabled hasEdits:YES]];
} @finally {if(owned)[self finish];}return ok;
}
- (BOOL)resetParameter:(UInt32)identifier {
NSDictionary *d=PGInspectorDescriptor(identifier);if(!d)return NO;
BOOL owned=!_interacting;if(owned)[self beginInteraction];if(!_interacting)return NO;
BOOL ok=NO;@try {
NSMutableDictionary *values=[_state.values mutableCopy];values[@(identifier)]=d[@"default"];
NSMutableSet *disabled=[_state.disabledParameters mutableCopy];[disabled removeObject:@(identifier)];
ok=[self commit:[PGInspectorState stateWithValues:values disabledParameters:disabled hasEdits:YES]];
} @finally {if(owned)[self finish];}return ok;
}
- (void)dealloc {if(_interacting)[_action endAction:self];}
@end
@@ -0,0 +1,16 @@
#import <AppKit/AppKit.h>
#import "PGInspectorState.h"
NS_ASSUME_NONNULL_BEGIN
/// Native inspector controls. Host access belongs exclusively to the controller.
@interface PGInspectorView : NSView
@property (nonatomic, strong, readonly) PGInspectorController *controller;
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager;
/// Also permits a deterministic controller in an offscreen preview or test.
- (instancetype)initWithController:(PGInspectorController *)controller;
- (void)refreshFromHost;
- (void)hostParameterChanged;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,348 @@
#import "PGInspectorView.h"
#include <math.h>
static NSColor *PGUIColor(CGFloat r, CGFloat g, CGFloat b, CGFloat a) {
return [NSColor colorWithSRGBRed:r green:g blue:b alpha:a];
}
static NSColor *PGUIText(void) { return PGUIColor(.88,.91,.95,1); }
static NSColor *PGUIMuted(void) { return PGUIColor(.60,.65,.73,1); }
static NSTextField *PGUILabel(NSString *text, CGFloat size, NSFontWeight weight) {
NSTextField *label=[NSTextField labelWithString:text ?: @""];
label.font=[NSFont systemFontOfSize:size weight:weight]; label.textColor=PGUIText();
label.lineBreakMode=NSLineBreakByTruncatingTail; return label;
}
static CGFloat PGValueScale(NSDictionary *descriptor) {
return [descriptor[@"format"] isEqual:@"percent"] && [descriptor[@"max"] doubleValue]<=10 ? 100 : 1;
}
static NSString *PGFormattedValue(NSDictionary *descriptor, double value) {
NSString *format=descriptor[@"format"];
if ([format isEqual:@"percent"]) return [NSString stringWithFormat:@"%.0f%%",value*PGValueScale(descriptor)];
if ([format isEqual:@"degrees"]) return [NSString stringWithFormat:@"%+.0f°",value];
if ([format isEqual:@"signed"]) return [NSString stringWithFormat:@"%.2f",value];
if ([format isEqual:@"integer"]) return [NSString stringWithFormat:@"%.0f",value];
return [NSString stringWithFormat:@"%.1f",value];
}
@interface PGInspectorGlyphButton : NSButton
@property (nonatomic, copy) NSString *glyph;
@property (nonatomic, strong) NSColor *accent;
@property (nonatomic, strong) NSColor *swatch;
@end
@implementation PGInspectorGlyphButton
- (BOOL)isFlipped { return YES; }
- (void)drawRect:(NSRect)dirtyRect {
(void)dirtyRect; NSRect b=NSInsetRect(self.bounds,1,1);
CGFloat opacity=self.enabled?1:.35;
NSColor *accent=[self.accent colorWithAlphaComponent:opacity];
BOOL on=self.state==NSControlStateValueOn;
if ([self.glyph isEqual:@"swatch"]) {
NSRect circle=NSInsetRect(b,3,3);
[[(self.swatch ?: PGUIColor(.12,.19,.23,1)) colorWithAlphaComponent:opacity] setFill]; [[NSBezierPath bezierPathWithOvalInRect:circle] fill];
[[PGUIText() colorWithAlphaComponent:.25*opacity] setStroke];
NSBezierPath *ring=[NSBezierPath bezierPathWithOvalInRect:circle];ring.lineWidth=1;[ring stroke];
if (!self.swatch) {
[PGUIMuted() setStroke];NSBezierPath *slash=[NSBezierPath bezierPath];
[slash moveToPoint:NSMakePoint(NSMinX(circle)+4,NSMinY(circle)+4)];[slash lineToPoint:NSMakePoint(NSMaxX(circle)-4,NSMaxY(circle)-4)];slash.lineWidth=1.5;[slash stroke];
}
if(on) { [accent setStroke];NSBezierPath *selection=[NSBezierPath bezierPathWithOvalInRect:NSInsetRect(b,.5,.5)];selection.lineWidth=1.5;[selection stroke]; }
} else if ([self.glyph isEqual:@"switch"]) {
NSRect pill=NSInsetRect(b,1,3);
[(on?accent:PGUIColor(.20,.25,.34,opacity)) setFill];[[NSBezierPath bezierPathWithRoundedRect:pill xRadius:NSHeight(pill)/2 yRadius:NSHeight(pill)/2]fill];
CGFloat diameter=NSHeight(pill)-4;CGFloat x=on?NSMaxX(pill)-diameter-2:NSMinX(pill)+2;
[[NSColor colorWithWhite:.98 alpha:opacity]setFill];[[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(x,NSMinY(pill)+2,diameter,diameter)]fill];
} else {
NSBezierPath *back=[NSBezierPath bezierPathWithRoundedRect:b xRadius:7 yRadius:7];
[[self.accent colorWithAlphaComponent:([self.glyph isEqual:@"eye"]?.055:.13)*opacity]setFill];[back fill];
if ([self.glyph isEqual:@"eye"]) {
[[self.accent colorWithAlphaComponent:.35*opacity]setStroke];back.lineWidth=.7;[back stroke];
NSPoint c=NSMakePoint(NSMidX(b),NSMidY(b));
NSBezierPath *eye=[NSBezierPath bezierPath];[eye moveToPoint:NSMakePoint(c.x-6,c.y)];
[eye curveToPoint:NSMakePoint(c.x+6,c.y) controlPoint1:NSMakePoint(c.x-3,c.y-5.5) controlPoint2:NSMakePoint(c.x+3,c.y-5.5)];
[eye curveToPoint:NSMakePoint(c.x-6,c.y) controlPoint1:NSMakePoint(c.x+3,c.y+5.5) controlPoint2:NSMakePoint(c.x-3,c.y+5.5)];eye.lineWidth=1.3;[accent setStroke];[eye stroke];
if(on){[accent setFill];[[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(c.x-1.8,c.y-1.8,3.6,3.6)]fill];}
else{NSBezierPath *slash=[NSBezierPath bezierPath];[slash moveToPoint:NSMakePoint(c.x-6,c.y+6)];[slash lineToPoint:NSMakePoint(c.x+6,c.y-6)];slash.lineWidth=1.2;[slash stroke];}
} else {
NSString *symbol=[self.glyph isEqual:@"disclosure"]?(on?@"⌄":@"›"):@"↻";
NSFont *font=[NSFont systemFontOfSize:[self.glyph isEqual:@"disclosure"]?19:18 weight:NSFontWeightSemibold];
NSDictionary *attributes=@{NSFontAttributeName:font,NSForegroundColorAttributeName:[self.accent colorWithAlphaComponent:(on?1:.58)*opacity]};
NSSize size=[symbol sizeWithAttributes:attributes];[symbol drawAtPoint:NSMakePoint(NSMidX(b)-size.width/2,NSMidY(b)-size.height/2)withAttributes:attributes];
}
}
if(self.window.firstResponder==self){[[self.accent colorWithAlphaComponent:.8]setStroke];NSBezierPath *focus=[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(self.bounds,.5,.5) xRadius:7 yRadius:7];focus.lineWidth=1;[focus stroke];}
}
- (void)setState:(NSControlStateValue)state { [super setState:state];self.needsDisplay=YES; }
- (void)setEnabled:(BOOL)enabled { [super setEnabled:enabled];self.needsDisplay=YES; }
@end
static PGInspectorGlyphButton *PGUIButton(NSString *glyph, NSColor *accent, id target, SEL action) {
PGInspectorGlyphButton *button=[[PGInspectorGlyphButton alloc]initWithFrame:NSZeroRect];
button.glyph=glyph;button.accent=accent;button.bordered=NO;button.title=@"";
[button setButtonType:([glyph isEqual:@"eye"]||[glyph isEqual:@"switch"]||[glyph isEqual:@"disclosure"]||[glyph isEqual:@"swatch"])?NSButtonTypePushOnPushOff:NSButtonTypeMomentaryPushIn];
button.target=target;button.action=action;button.focusRingType=NSFocusRingTypeExterior;
return button;
}
@interface PGInspectorSliderCell : NSSliderCell
@property (nonatomic, strong) NSColor *accent;
@end
@implementation PGInspectorSliderCell
- (void)drawBarInside:(NSRect)rect flipped:(BOOL)flipped {
(void)flipped;NSRect bar=NSMakeRect(NSMinX(rect),NSMidY(rect)-2,NSWidth(rect),4);
[PGUIColor(.08,.13,.20,self.enabled?1:.5)setFill];NSBezierPath *track=[NSBezierPath bezierPathWithRoundedRect:bar xRadius:2 yRadius:2];[track fill];
[[self.accent colorWithAlphaComponent:.18]setStroke];track.lineWidth=.7;[track stroke];
double range=self.maxValue-self.minValue;if(range<=0)return;
CGFloat from=self.minValue<0?(CGFloat)((0-self.minValue)/range):0;
CGFloat to=(CGFloat)((self.doubleValue-self.minValue)/range);
NSRect fill=NSMakeRect(NSMinX(bar)+MIN(from,to)*NSWidth(bar),NSMinY(bar),fabs(to-from)*NSWidth(bar),NSHeight(bar));
[[self.accent colorWithAlphaComponent:self.enabled?.85:.23]setFill];[[NSBezierPath bezierPathWithRoundedRect:fill xRadius:2 yRadius:2]fill];
}
- (void)drawKnob:(NSRect)knobRect {
CGFloat d=12;NSRect r=NSMakeRect(NSMidX(knobRect)-d/2,NSMidY(knobRect)-d/2,d,d);
[[self.accent colorWithAlphaComponent:self.enabled?.22:.08]setFill];[[NSBezierPath bezierPathWithOvalInRect:NSInsetRect(r,-2,-2)]fill];
[[NSColor colorWithWhite:.97 alpha:self.enabled?1:.5]setFill];NSBezierPath *knob=[NSBezierPath bezierPathWithOvalInRect:r];[knob fill];
[[self.accent colorWithAlphaComponent:self.enabled?.85:.25]setStroke];knob.lineWidth=1;[knob stroke];
}
- (CGFloat)knobThickness { return 14; }
@end
@interface PGInspectorSlider : NSSlider
@property (nonatomic, copy) void (^interactionBegan)(void);
@property (nonatomic, copy) void (^interactionEnded)(void);
@end
@implementation PGInspectorSlider
- (void)mouseDown:(NSEvent *)event {
if(!self.enabled)return;if(self.interactionBegan)self.interactionBegan();
@try{[super mouseDown:event];}@finally{if(self.interactionEnded)self.interactionEnded();}
}
- (void)keyDown:(NSEvent *)event {
if(self.interactionBegan)self.interactionBegan();
@try{[super keyDown:event];}@finally{if(self.interactionEnded)self.interactionEnded();}
}
@end
@interface PGInspectorNumberCell : NSTextFieldCell
@end
@implementation PGInspectorNumberCell
- (NSRect)drawingRectForBounds:(NSRect)bounds {
NSRect rect=[super drawingRectForBounds:bounds];CGFloat height=[self cellSizeForBounds:bounds].height;
if(NSHeight(rect)>height){rect.origin.y+=(NSHeight(rect)-height)*.5;rect.size.height=height;}return rect;
}
@end
@interface PGInspectorNumberField : NSTextField
@end
@implementation PGInspectorNumberField
+ (Class)cellClass { return PGInspectorNumberCell.class; }
@end
@class PGInspectorRow;
@interface PGInspectorCard : NSView
@property (nonatomic, strong) NSColor *accent;
@property (nonatomic, copy) NSString *heading;
@property (nonatomic, strong) NSTextField *titleLabel;
@property (nonatomic, strong) PGInspectorGlyphButton *master;
@property (nonatomic, copy) NSArray<PGInspectorRow *> *rows;
@property (nonatomic) BOOL expanded;
- (CGFloat)heightForWidth:(CGFloat)width;
@end
@interface PGInspectorView ()
@property (nonatomic, strong, readwrite) PGInspectorController *controller;
- (void)reloadControls;
- (void)beginControlInteraction;
- (void)endControlInteraction;
- (void)writeValue:(double)value parameter:(UInt32)identifier;
@end
@interface PGInspectorRow : NSView <NSTextFieldDelegate>
@property (nonatomic, weak) PGInspectorView *inspector;
@property (nonatomic, copy) NSDictionary *descriptor;
@property (nonatomic, strong) NSColor *accent;
@property (nonatomic, strong) NSTextField *label;
@property (nonatomic, strong) NSTextField *valueField;
@property (nonatomic, strong) NSTextField *hint;
@property (nonatomic, strong) PGInspectorSlider *slider;
@property (nonatomic, strong) PGInspectorGlyphButton *eye;
@property (nonatomic, strong) PGInspectorGlyphButton *reset;
@property (nonatomic, strong) PGInspectorGlyphButton *toggle;
@property (nonatomic, copy) NSArray<PGInspectorGlyphButton *> *swatches;
- (instancetype)initWithDescriptor:(NSDictionary *)descriptor accent:(NSColor *)accent inspector:(PGInspectorView *)inspector;
- (CGFloat)heightForWidth:(CGFloat)width;
- (void)reloadState:(PGInspectorState *)state master:(BOOL)master available:(BOOL)available;
@end
@implementation PGInspectorRow
- (BOOL)isFlipped { return YES; }
- (instancetype)initWithDescriptor:(NSDictionary *)descriptor accent:(NSColor *)accent inspector:(PGInspectorView *)inspector {
if((self=[super initWithFrame:NSZeroRect])) {
_descriptor=[descriptor copy];_accent=accent;_inspector=inspector;
self.identifier=[NSString stringWithFormat:@"pg.parameter.%@",descriptor[@"id"]];
NSString *name=descriptor[@"name"];_label=PGUILabel(name,12,NSFontWeightSemibold);[self addSubview:_label];
NSString *kind=descriptor[@"kind"];
if([descriptor[@"bypass"]boolValue]){
_eye=PGUIButton(@"eye",accent,self,@selector(toggleEnabled:));_eye.toolTip=[@"启用 / 停用 · " stringByAppendingString:name];_eye.accessibilityLabel=_eye.toolTip;[self addSubview:_eye];
}
if([kind isEqual:@"preset"]){
NSArray *colors=@[NSNull.null,PGUIColor(.95,.87,.80,1),PGUIColor(.88,.88,.95,1),PGUIColor(1,.86,.72,1),PGUIColor(.99,.78,.75,1),PGUIColor(.76,.57,.39,1),PGUIColor(.48,.32,.22,1)];
NSArray *options=descriptor[@"options"] ?: @[];NSMutableArray *buttons=[NSMutableArray new];
for(NSUInteger i=0;i<7;i++){
PGInspectorGlyphButton *button=PGUIButton(@"swatch",accent,self,@selector(selectPreset:));button.tag=i;button.swatch=i?colors[i]:nil;
button.toolTip=i<options.count?options[i]:[NSString stringWithFormat:@"%@ %lu",name,(unsigned long)i];button.accessibilityLabel=button.toolTip;
[self addSubview:button];[buttons addObject:button];
}_swatches=[buttons copy];
}else{
_reset=PGUIButton(@"reset",accent,self,@selector(resetValue:));_reset.toolTip=[@"重置 · "stringByAppendingString:name];_reset.accessibilityLabel=_reset.toolTip;[self addSubview:_reset];
if([kind isEqual:@"toggle"]){
_toggle=PGUIButton(@"switch",accent,self,@selector(toggleValue:));_toggle.accessibilityLabel=name;[self addSubview:_toggle];
_hint=PGUILabel(@"",10,NSFontWeightMedium);_hint.textColor=PGUIMuted();_hint.alignment=NSTextAlignmentRight;[self addSubview:_hint];
_label.lineBreakMode=NSLineBreakByWordWrapping;_label.maximumNumberOfLines=2;
}else{
_slider=[[PGInspectorSlider alloc]initWithFrame:NSZeroRect];PGInspectorSliderCell *cell=[PGInspectorSliderCell new];cell.accent=accent;_slider.cell=cell;
_slider.minValue=[descriptor[@"min"]doubleValue];_slider.maxValue=[descriptor[@"max"]doubleValue];_slider.continuous=YES;_slider.target=self;_slider.action=@selector(sliderChanged:);_slider.accessibilityLabel=name;
__weak PGInspectorView *weakInspector=inspector;
_slider.interactionBegan=^{[weakInspector beginControlInteraction];};_slider.interactionEnded=^{[weakInspector endControlInteraction];};[self addSubview:_slider];
_valueField=[[PGInspectorNumberField alloc]initWithFrame:NSZeroRect];_valueField.font=[NSFont monospacedDigitSystemFontOfSize:11 weight:NSFontWeightSemibold];_valueField.textColor=PGUIText();_valueField.alignment=NSTextAlignmentCenter;
_valueField.bordered=NO;_valueField.bezeled=NO;_valueField.drawsBackground=NO;_valueField.backgroundColor=PGUIColor(.14,.19,.26,1);_valueField.wantsLayer=YES;_valueField.layer.backgroundColor=_valueField.backgroundColor.CGColor;_valueField.layer.cornerRadius=7;_valueField.layer.borderWidth=.7;_valueField.layer.borderColor=[accent colorWithAlphaComponent:.24].CGColor;
_valueField.target=self;_valueField.action=@selector(numberCommitted:);_valueField.delegate=self;_valueField.accessibilityLabel=[name stringByAppendingString:@"数值"];[self addSubview:_valueField];
}
}
NSString *key=[descriptor[@"id"]stringValue];
_slider.identifier=[@"pg.value."stringByAppendingString:key];_valueField.identifier=[@"pg.number."stringByAppendingString:key];
_toggle.identifier=[@"pg.toggle."stringByAppendingString:key];_eye.identifier=[@"pg.enable."stringByAppendingString:key];_reset.identifier=[@"pg.reset."stringByAppendingString:key];
}return self;
}
- (CGFloat)heightForWidth:(CGFloat)width {
if(_swatches)return 46;
if(_toggle)return width<310 && [_descriptor[@"name"]length]>12 ? 48 : 36;
return width<310?52:36;
}
- (void)layout {
[super layout];CGFloat w=NSWidth(self.bounds),h=NSHeight(self.bounds);BOOL narrow=w<310;
CGFloat labelX=_eye?25:0;
_eye.frame=NSMakeRect(0,6,20,22);
if(_swatches){
_label.frame=NSMakeRect(labelX,14,33,20);CGFloat x=labelX+37;CGFloat d=MIN(28,MAX(12,(w-x-24)/7));CGFloat gap=MIN(8,MAX(0,(w-x-7*d)/6));
for(NSUInteger i=0;i<_swatches.count;i++)_swatches[i].frame=NSMakeRect(x+i*(d+gap),7,d,d);return;
}
_reset.frame=NSMakeRect(w-23,6,23,23);
if(_toggle){
CGFloat switchX=w-76;_toggle.frame=NSMakeRect(switchX,4+(h-36)/2,46,28);_hint.frame=NSMakeRect(switchX-39,10+(h-36)/2,34,18);
_label.frame=NSMakeRect(labelX,h>36?3:9,MAX(20,switchX-labelX-42),h>36?42:22);return;
}
CGFloat fieldW=49,fieldX=w-23-7-fieldW;_valueField.frame=NSMakeRect(fieldX,5,fieldW,25);
if(narrow){_label.frame=NSMakeRect(labelX,8,MAX(20,fieldX-labelX-6),22);_slider.frame=NSMakeRect(labelX,29,MAX(20,w-labelX-30),18);}
else{CGFloat labelW=MIN(116,MAX(71,w*.29));_label.frame=NSMakeRect(labelX,9,labelW,21);CGFloat start=labelX+labelW+5;_slider.frame=NSMakeRect(start,7,MAX(20,fieldX-start-8),22);}
}
- (void)reloadState:(PGInspectorState *)state master:(BOOL)master available:(BOOL)available {
UInt32 identifier=[_descriptor[@"id"]unsignedIntValue];double value=[state valueForParameter:identifier];BOOL enabled=[state isParameterEnabled:identifier];
_label.alphaValue=master&&enabled?1:.5;_eye.state=enabled?NSControlStateValueOn:NSControlStateValueOff;_eye.enabled=available&&master;
_reset.enabled=available;_reset.state=fabs(value-[_descriptor[@"default"]doubleValue])>.000001?NSControlStateValueOn:NSControlStateValueOff;
_slider.enabled=available&&master&&enabled;_slider.doubleValue=value;
_valueField.enabled=available&&master&&enabled;if(!_valueField.currentEditor)_valueField.stringValue=PGFormattedValue(_descriptor,value);
_toggle.enabled=available&&master&&enabled;_toggle.state=value>.5?NSControlStateValueOn:NSControlStateValueOff;
UInt32 identifierForHint=[_descriptor[@"id"]unsignedIntValue];
_hint.stringValue=identifierForHint==PGFastMulti?(value>.5?@"多人":@"单人"):identifierForHint==PGTrackBoxes?(value>.5?@"预览中":@"隐藏"):(value>.5?@"开启":@"关闭");
for(NSUInteger i=0;i<_swatches.count;i++){_swatches[i].enabled=available&&master&&enabled;_swatches[i].state=i==(NSUInteger)llround(value)?NSControlStateValueOn:NSControlStateValueOff;}
}
- (void)sliderChanged:(NSSlider *)sender { [_inspector writeValue:sender.doubleValue parameter:[_descriptor[@"id"]unsignedIntValue]]; }
- (void)toggleValue:(NSButton *)sender { [_inspector writeValue:sender.state==NSControlStateValueOn?1:0 parameter:[_descriptor[@"id"]unsignedIntValue]]; }
- (void)selectPreset:(NSButton *)sender { [_inspector writeValue:sender.tag parameter:[_descriptor[@"id"]unsignedIntValue]]; }
- (void)toggleEnabled:(id)sender { (void)sender;[_inspector.controller toggleEnabledForParameter:[_descriptor[@"id"]unsignedIntValue]];[_inspector reloadControls]; }
- (void)resetValue:(id)sender { (void)sender;[_inspector.controller resetParameter:[_descriptor[@"id"]unsignedIntValue]];[_inspector reloadControls]; }
- (void)numberCommitted:(id)sender {
(void)sender;NSString *text=[_valueField.stringValue stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
if([text hasSuffix:@"%"]||[text hasSuffix:@"°"])text=[text substringToIndex:text.length-1];
NSNumber *number=nil;
for(NSLocale *locale in @[NSLocale.currentLocale,[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]]){
NSScanner *scanner=[NSScanner scannerWithString:text];scanner.locale=locale;double parsed;
if([scanner scanDouble:&parsed]&&scanner.isAtEnd){number=@(parsed);break;}
}
if(number&&isfinite(number.doubleValue)){
double value=number.doubleValue/PGValueScale(_descriptor);value=fmin([_descriptor[@"max"]doubleValue],fmax([_descriptor[@"min"]doubleValue],value));
// Commit explicit input even when it equals our last displayed value:
// the document may have changed while the field editor held focus.
// The controller rereads the host and deduplicates the actual value.
[_inspector writeValue:value parameter:[_descriptor[@"id"]unsignedIntValue]];
}else NSBeep();
_valueField.stringValue=PGFormattedValue(_descriptor,[_inspector.controller.state valueForParameter:[_descriptor[@"id"]unsignedIntValue]]);
}
- (void)controlTextDidEndEditing:(NSNotification *)notification { (void)notification;[self numberCommitted:_valueField]; }
@end
@implementation PGInspectorCard
- (BOOL)isFlipped { return YES; }
- (CGFloat)heightForWidth:(CGFloat)width {
if(!_expanded)return 44;CGFloat height=49;for(PGInspectorRow *row in _rows){height+=[row heightForWidth:MAX(0,width-24)];if([row.descriptor[@"id"]unsignedIntValue]==PGJumpGuard)height+=8;}return height+7;
}
- (void)layout {
[super layout];CGFloat w=NSWidth(self.bounds);_titleLabel.frame=NSMakeRect(25,13,MAX(70,w-80),22);_master.frame=NSMakeRect(w-39,10,25,25);
CGFloat y=49;for(PGInspectorRow *row in _rows){row.hidden=!_expanded;if(!_expanded)continue;CGFloat h=[row heightForWidth:w-24];row.frame=NSMakeRect(12,y,w-24,h);y+=h;if([row.descriptor[@"id"]unsignedIntValue]==PGJumpGuard)y+=8;}
}
- (void)drawRect:(NSRect)dirtyRect {
(void)dirtyRect;NSBezierPath *shape=[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(self.bounds,.5,.5) xRadius:13 yRadius:13];
[NSGraphicsContext saveGraphicsState];[shape addClip];NSGradient *gradient=[[NSGradient alloc]initWithStartingColor:[_accent blendedColorWithFraction:.87 ofColor:PGUIColor(.09,.14,.21,1)] endingColor:PGUIColor(.055,.085,.14,1)];[gradient drawInRect:self.bounds angle:90];[NSGraphicsContext restoreGraphicsState];
[[_accent colorWithAlphaComponent:.22]setStroke];shape.lineWidth=.8;[shape stroke];
NSRect pill=NSMakeRect(12,10,MAX(53,[_heading length]*13+20),25);[[_accent colorWithAlphaComponent:.10]setFill];NSBezierPath *p=[NSBezierPath bezierPathWithRoundedRect:pill xRadius:8 yRadius:8];[p fill];[[_accent colorWithAlphaComponent:.32]setStroke];p.lineWidth=.7;[p stroke];
if(_expanded){[[PGUIMuted() colorWithAlphaComponent:.20]setStroke];NSBezierPath *line=[NSBezierPath bezierPath];[line moveToPoint:NSMakePoint(12,43)];[line lineToPoint:NSMakePoint(NSWidth(self.bounds)-12,43)];line.lineWidth=.5;[line stroke];
for(PGInspectorRow *row in _rows)if([row.descriptor[@"id"]unsignedIntValue]==PGJumpGuard){CGFloat y=NSMaxY(row.frame)+4;[line removeAllPoints];[line moveToPoint:NSMakePoint(12,y)];[line lineToPoint:NSMakePoint(NSWidth(self.bounds)-12,y)];[line stroke];}
}
}
@end
@implementation PGInspectorView {
NSArray<PGInspectorCard *> *_cards;
PGInspectorCard *_more;
NSTextField *_brand,*_website,*_status;
NSTimer *_refreshTimer;
NSUInteger _interactionDepth;
}
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager { return [self initWithController:[[PGInspectorController alloc]initWithAPIManager:manager]]; }
- (instancetype)initWithController:(PGInspectorController *)controller {
if((self=[super initWithFrame:NSMakeRect(0,0,320,100)])){
_controller=controller;self.appearance=[NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
NSArray *sections=@[@"smooth",@"color",@"shape",@"more"],*headings=@[@"磨皮",@"美白",@"瘦脸",@"更多设置"];
NSArray *colors=@[PGUIColor(.28,.83,.96,1),PGUIColor(.34,.89,.84,1),PGUIColor(.59,.69,.98,1),PGUIColor(.54,.64,.76,1)];
NSMutableArray *cards=[NSMutableArray new];
for(NSUInteger index=0;index<sections.count;index++){
NSString *section=sections[index];PGInspectorCard *card=[[PGInspectorCard alloc]initWithFrame:NSZeroRect];card.accent=colors[index];card.heading=headings[index];card.expanded=index<3;
card.titleLabel=PGUILabel(headings[index],12,NSFontWeightSemibold);card.titleLabel.textColor=colors[index];[card addSubview:card.titleLabel];NSMutableArray *rows=[NSMutableArray new];
for(NSDictionary *descriptor in PGInspectorControls()){
if(![descriptor[@"section"]isEqual:section])continue;
if([descriptor[@"kind"]isEqual:@"master"]){card.master=PGUIButton(@"eye",colors[index],self,@selector(masterChanged:));card.master.tag=[descriptor[@"id"]integerValue];card.master.identifier=[NSString stringWithFormat:@"pg.master.%@",descriptor[@"id"]];card.master.accessibilityLabel=descriptor[@"name"];card.master.toolTip=descriptor[@"name"];[card addSubview:card.master];continue;}
PGInspectorRow *row=[[PGInspectorRow alloc]initWithDescriptor:descriptor accent:colors[index] inspector:self];[rows addObject:row];[card addSubview:row];
}
card.rows=rows;if(index==3){_more=card;card.master=PGUIButton(@"disclosure",colors[index],self,@selector(moreChanged:));card.master.identifier=@"pg.more";card.master.accessibilityLabel=@"展开更多设置";[card addSubview:card.master];}
[self addSubview:card];[cards addObject:card];
}_cards=[cards copy];
_brand=PGUILabel(@"大鱼 · 人像美化",15,NSFontWeightSemibold);_brand.alignment=NSTextAlignmentCenter;[self addSubview:_brand];
_website=PGUILabel(@"dayu.media",10,NSFontWeightMedium);_website.alignment=NSTextAlignmentCenter;_website.textColor=PGUIMuted();[self addSubview:_website];
_status=PGUILabel(@"",10,NSFontWeightRegular);_status.textColor=PGUIMuted();_status.alignment=NSTextAlignmentCenter;[self addSubview:_status];
[self reloadControls];[self setFrameSize:NSMakeSize(320,[self heightForWidth:320])];
}return self;
}
- (BOOL)isFlipped { return YES; }
- (CGFloat)heightForWidth:(CGFloat)width { CGFloat h=12;for(PGInspectorCard *card in _cards)h+=[card heightForWidth:MAX(0,width-20)]+10;return h+83; }
- (NSSize)intrinsicContentSize { return NSMakeSize(NSViewNoIntrinsicMetric,[self heightForWidth:NSWidth(self.bounds)>0?NSWidth(self.bounds):320]); }
- (NSSize)fittingSize { CGFloat w=NSWidth(self.bounds)>0?NSWidth(self.bounds):320;return NSMakeSize(w,[self heightForWidth:w]); }
- (void)setFrameSize:(NSSize)newSize { if(_cards)newSize.height=[self heightForWidth:newSize.width];[super setFrameSize:newSize];[self invalidateIntrinsicContentSize];self.needsLayout=YES; }
- (void)layout {
[super layout];CGFloat w=NSWidth(self.bounds),y=12;for(PGInspectorCard *card in _cards){CGFloat h=[card heightForWidth:w-20];card.frame=NSMakeRect(10,y,MAX(0,w-20),h);y+=h+10;}
_brand.frame=NSMakeRect(12,y+12,MAX(0,w-24),23);_website.frame=NSMakeRect(12,y+38,MAX(0,w-24),17);_status.frame=NSMakeRect(12,y+61,MAX(0,w-24),16);
}
- (void)drawRect:(NSRect)dirtyRect { [PGUIColor(.035,.060,.105,1)setFill];NSRectFill(dirtyRect); }
- (void)reloadControls {
PGInspectorState *state=_controller.state;BOOL available=_controller.available;
for(PGInspectorCard *card in _cards){BOOL master=card==_more || [state valueForParameter:(UInt32)card.master.tag]>.5;
if(card!=_more){card.master.state=master?NSControlStateValueOn:NSControlStateValueOff;card.master.enabled=available;}
for(PGInspectorRow *row in card.rows)[row reloadState:state master:master available:available];
}
_status.stringValue=_controller.statusMessage.length?_controller.statusMessage:@"自动跟随 · 无需手动分析";_status.toolTip=_status.stringValue;
}
- (BOOL)hasActiveTextEditor { for(PGInspectorCard *card in _cards)for(PGInspectorRow *row in card.rows)if(row.valueField.currentEditor)return YES;return NO; }
- (void)refreshFromHost { if(_interactionDepth || [self hasActiveTextEditor])return;[_controller refresh];[self reloadControls]; }
- (void)hostParameterChanged { if(_interactionDepth || [self hasActiveTextEditor])return;[_controller documentDidChange];[self reloadControls]; }
- (void)writeValue:(double)value parameter:(UInt32)identifier { [_controller setValue:value forParameter:identifier];[self reloadControls]; }
- (void)beginControlInteraction { if(_interactionDepth++==0)[_controller beginInteraction]; }
- (void)endControlInteraction { if(!_interactionDepth)return;if(--_interactionDepth==0){[_controller endInteraction];[self refreshFromHost];} }
- (void)masterChanged:(NSButton *)sender { [_controller setValue:sender.state==NSControlStateValueOn?1:0 forParameter:(UInt32)sender.tag];[self reloadControls]; }
- (void)moreChanged:(NSButton *)sender { _more.expanded=sender.state==NSControlStateValueOn;sender.accessibilityLabel=_more.expanded?@"收起更多设置":@"展开更多设置";[_more setNeedsDisplay:YES];[self setFrameSize:self.frame.size]; }
- (void)viewDidMoveToWindow {
[super viewDidMoveToWindow];[_refreshTimer invalidate];_refreshTimer=nil;
if(self.window){[self refreshFromHost];__weak PGInspectorView *weakSelf=self;_refreshTimer=[NSTimer timerWithTimeInterval:.65 repeats:YES block:^(NSTimer *timer){PGInspectorView *view=weakSelf;if(!view||!view.window){[timer invalidate];return;}[view refreshFromHost];}];[[NSRunLoop mainRunLoop]addTimer:_refreshTimer forMode:NSRunLoopCommonModes];}
}
- (void)dealloc { [_refreshTimer invalidate]; }
@end
+2 -1
查看文件
@@ -13,5 +13,6 @@ enum {
PGAnalyzeForward=50, PGAnalyzeBackward=51, PGClearTrack=52, PGStatus=53, PGAnalyzeForward=50, PGAnalyzeBackward=51, PGClearTrack=52, PGStatus=53,
PGTrackData1=60, PGTrackData2=61, PGTrackData3=62, PGTrackData1=60, PGTrackData2=61, PGTrackData3=62,
PGSmoothingEnabled=70, PGColorEnabled=71, PGShapeEnabled=72, PGSmoothingEnabled=70, PGColorEnabled=71, PGShapeEnabled=72,
PGDetailEnabled=73, PGHelp=74 PGDetailEnabled=73, PGHelp=74,
PGInspectorPanel=210
}; };
+1 -1
查看文件
@@ -2,7 +2,7 @@
#import <FxPlug/FxPlugSDK.h> #import <FxPlug/FxPlugSDK.h>
#import "PGBeautyTypes.h" #import "PGBeautyTypes.h"
@interface beatygrilPlugIn : NSObject <FxTileableEffect, FxAnalyzer> @interface beatygrilPlugIn : NSObject <FxTileableEffect, FxAnalyzer, FxCustomParameterViewHost_v2>
@property (nonatomic, assign) id<PROAPIAccessing> apiManager; @property (nonatomic, assign) id<PROAPIAccessing> apiManager;
- (PGBeautySettings)settingsAtTime:(CMTime)time; - (PGBeautySettings)settingsAtTime:(CMTime)time;
- (void)analyzeForward; - (void)analyzeForward;
+65 -25
查看文件
@@ -2,6 +2,8 @@
#import "PGParameterIDs.h" #import "PGParameterIDs.h"
#import "PGBeautyRenderer.h" #import "PGBeautyRenderer.h"
#import "PGFaceProcessor.h" #import "PGFaceProcessor.h"
#import "PGInspectorState.h"
#import "PGInspectorView.h"
#import <Metal/Metal.h> #import <Metal/Metal.h>
#import <IOSurface/IOSurface.h> #import <IOSurface/IOSurface.h>
#import <stddef.h> #import <stddef.h>
@@ -52,15 +54,29 @@ static BOOL PGFail(NSError **error, NSString *message) {
userInfo:@{NSLocalizedDescriptionKey:message}]; userInfo:@{NSLocalizedDescriptionKey:message}];
return NO; return NO;
} }
static NSString * const PGHostCoordinateSpace = @"iosurface-ci-y-up-v1";
static BOOL PGCompatibleHostTrack(NSData *data) {
if (!data.length || data.length > 128*1024*1024) return NO;
NSDictionary *archive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:nil error:nil];
return [archive isKindOfClass:NSDictionary.class] && [archive[@"hostCoordinateSpace"] isEqual:PGHostCoordinateSpace];
}
static NSData *PGMarkHostTrack(NSData *data) {
if (!data) return nil;
NSMutableDictionary *archive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListMutableContainers format:nil error:nil];
if (![archive isKindOfClass:NSMutableDictionary.class]) return nil;
archive[@"hostCoordinateSpace"] = PGHostCoordinateSpace;
return [NSPropertyListSerialization dataWithPropertyList:archive format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
}
static CIImage *PGImageFromTile(FxImageTile *tile) { static CIImage *PGImageFromTile(FxImageTile *tile) {
if (!tile.ioSurface) return nil; if (!tile.ioSurface) return nil;
// The host already supplies video-gamma RGB. Keep those numeric values in // The host already supplies video-gamma RGB. Keep those numeric values in
// every stage; implicit Core Image linearization changes mask thresholds. // every stage; implicit Core Image linearization changes mask thresholds.
NSDictionary *options = @{kCIImageColorSpace:NSNull.null}; NSDictionary *options = @{kCIImageColorSpace:NSNull.null};
CIImage *image = [CIImage imageWithIOSurface:(__bridge IOSurfaceRef)tile.ioSurface options:options]; CIImage *image = [CIImage imageWithIOSurface:(__bridge IOSurfaceRef)tile.ioSurface options:options];
if (tile.imageOrigin == kFxImageOrigin_TOP_LEFT) { // Match the reference's CI/IOSurface path. imageOrigin describes the host's
image = [image imageByApplyingTransform:CGAffineTransformMake(1,0,0,-1,0,image.extent.size.height)]; // coordinate axes; it is not an instruction to reverse IOSurface scanlines.
} // An extra input/output flip can hide in passthrough while inverting faces
// inside Vision and the analytic shape/mask stages.
FxRect b = tile.tilePixelBounds; FxRect b = tile.tilePixelBounds;
return [image imageByApplyingTransform:CGAffineTransformMakeTranslation(b.left,b.bottom)]; return [image imageByApplyingTransform:CGAffineTransformMakeTranslation(b.left,b.bottom)];
} }
@@ -77,6 +93,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
@property BOOL analysisForward; @property BOOL analysisForward;
@property (strong) NSArray<NSString *> *loadedTracks; @property (strong) NSArray<NSString *> *loadedTracks;
@property BOOL reportedRender; @property BOOL reportedRender;
@property (strong) NSHashTable<PGInspectorView *> *inspectorViews;
@end @end
@implementation beatygrilPlugIn @implementation beatygrilPlugIn
@@ -87,6 +104,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
_analysisFaceProcessor=[PGFaceProcessor new]; _analysisFaceProcessor=[PGFaceProcessor new];
_contexts=[NSMutableDictionary new]; _analysisSettings=PGBeautyDefaults(); _contexts=[NSMutableDictionary new]; _analysisSettings=PGBeautyDefaults();
_analysisTime=kCMTimeZero; _analysisTime=kCMTimeZero;
_inspectorViews=[NSHashTable weakObjectsHashTable];
} }
return self; return self;
} }
@@ -105,8 +123,8 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
PGSlider spec=PGSliders[i]; if (spec.identifier!=identifier) continue; PGSlider spec=PGSliders[i]; if (spec.identifier!=identifier) continue;
NSString *name=[NSString stringWithUTF8String:spec.name]; NSString *name=[NSString stringWithUTF8String:spec.name];
float value=*(float *)((uint8_t *)&defaults+spec.offset); float value=*(float *)((uint8_t *)&defaults+spec.offset);
if (spec.percent) return [api addPercentSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_DEFAULT]; if (spec.percent) return [api addPercentSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_HIDDEN];
return [api addFloatSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_DEFAULT]; return [api addFloatSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_HIDDEN];
} }
return NO; return NO;
} }
@@ -114,37 +132,56 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
id<FxParameterCreationAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterCreationAPI_v5)]; id<FxParameterCreationAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterCreationAPI_v5)];
if (!api) return PGFail(error,@"无法取得 FxPlug 参数创建接口。"); if (!api) return PGFail(error,@"无法取得 FxPlug 参数创建接口。");
__block BOOL ok=YES; __block BOOL ok=YES;
ok=[api addCustomParameterWithName:@"" parameterID:PGInspectorPanel
defaultValue:[PGInspectorState stateWithValues:@{} disabledParameters:[NSSet set] hasEdits:NO]
parameterFlags:kFxParameterFlag_CUSTOM_UI|kFxParameterFlag_USE_FULL_VIEW_WIDTH]&&ok;
void (^slider)(UInt32)=^(UInt32 identifier){ok=[self addSlider:identifier api:api]&&ok;}; void (^slider)(UInt32)=^(UInt32 identifier){ok=[self addSlider:identifier api:api]&&ok;};
void (^toggle)(NSString *,UInt32,BOOL)=^(NSString *name,UInt32 identifier,BOOL value){ok=[api addToggleButtonWithName:name parameterID:identifier defaultValue:value parameterFlags:kFxParameterFlag_DEFAULT]&&ok;}; void (^toggle)(NSString *,UInt32,BOOL)=^(NSString *name,UInt32 identifier,BOOL value){ok=[api addToggleButtonWithName:name parameterID:identifier defaultValue:value parameterFlags:kFxParameterFlag_HIDDEN]&&ok;};
void (^group)(NSString *,UInt32)=^(NSString *name,UInt32 identifier){ok=[api startParameterSubGroup:name parameterID:identifier parameterFlags:kFxParameterFlag_DEFAULT]&&ok;}; void (^group)(NSString *,UInt32)=^(NSString *name,UInt32 identifier){ok=[api startParameterSubGroup:name parameterID:identifier parameterFlags:kFxParameterFlag_HIDDEN]&&ok;};
void (^end)(void)=^{ok=[api endParameterSubGroup]&&ok;}; void (^end)(void)=^{ok=[api endParameterSubGroup]&&ok;};
group(@"磨皮",100); toggle(@"启用磨皮",PGSmoothingEnabled,YES); group(@"磨皮",100); toggle(@"启用磨皮",PGSmoothingEnabled,YES);
slider(PGAmount); slider(PGMidtone); slider(PGRadius); toggle(@"保护五官",PGFeatureProtect,YES); end(); slider(PGAmount); slider(PGMidtone); slider(PGRadius); toggle(@"保护五官",PGFeatureProtect,YES); end();
group(@"肤色",101); toggle(@"启用肤色",PGColorEnabled,YES); group(@"肤色",101); toggle(@"启用肤色",PGColorEnabled,YES);
ok=[api addPopupMenuWithName:@"肤色风格" parameterID:PGTonePreset defaultValue:0 menuEntries:@[@"原色",@"自然白",@"冷白",@"暖白",@"红润白",@"小麦",@"古铜"] parameterFlags:0]&&ok; ok=[api addPopupMenuWithName:@"肤色风格" parameterID:PGTonePreset defaultValue:0 menuEntries:@[@"原色",@"自然白",@"冷白",@"暖白",@"红润白",@"小麦",@"古铜"] parameterFlags:kFxParameterFlag_HIDDEN]&&ok;
slider(PGBrightness); slider(PGHue); slider(PGSaturation); slider(PGEvenTone); slider(PGTeeth); slider(PGEyeBright); end(); slider(PGBrightness); slider(PGHue); slider(PGSaturation); slider(PGEvenTone); slider(PGTeeth); slider(PGEyeBright); end();
group(@"细节",102); toggle(@"启用细节",PGDetailEnabled,YES); slider(PGHighPass); slider(PGSharpness); end(); group(@"细节",102); toggle(@"启用细节",PGDetailEnabled,YES); slider(PGHighPass); slider(PGSharpness); end();
group(@"脸型",103); toggle(@"启用脸型",PGShapeEnabled,YES); group(@"脸型",103); toggle(@"启用脸型",PGShapeEnabled,YES);
UInt32 shape[]={PGThin,PGBigEye,PGShort,PGNarrow,PGChin,PGVFace,PGJaw,PGCheekbone,PGSmallHead,PGSmallHeadRange}; UInt32 shape[]={PGThin,PGBigEye,PGShort,PGNarrow,PGChin,PGVFace,PGJaw,PGCheekbone,PGSmallHead,PGSmallHeadRange};
for(NSUInteger i=0;i<sizeof(shape)/sizeof(UInt32);i++) slider(shape[i]); end(); for(NSUInteger i=0;i<sizeof(shape)/sizeof(UInt32);i++) slider(shape[i]); end();
group(@"人脸选择与跟踪",104); toggle(@"只处理选中人脸",PGSingleFace,NO); group(@"人脸选择与跟踪",104); toggle(@"只处理选中人脸",PGSingleFace,NO);
ok=[api addPointParameterWithName:@"选脸位置" parameterID:PGSelection defaultX:.5 defaultY:.5 parameterFlags:0]&&ok; ok=[api addPointParameterWithName:@"选脸位置" parameterID:PGSelection defaultX:.5 defaultY:.5 parameterFlags:kFxParameterFlag_HIDDEN]&&ok;
toggle(@"多人高速模式",PGFastMulti,NO); slider(PGJumpGuard); toggle(@"多人高速模式",PGFastMulti,NO); slider(PGJumpGuard);
toggle(@"使用跟踪数据",PGTrackEnabled,NO); toggle(@"使用跟踪数据",PGTrackEnabled,NO);
ok=[api addPopupMenuWithName:@"跟踪目标" parameterID:PGTarget defaultValue:0 menuEntries:@[@"目标 1",@"目标 2",@"目标 3"] parameterFlags:kFxParameterFlag_NOT_ANIMATABLE]&&ok; ok=[api addPopupMenuWithName:@"跟踪目标" parameterID:PGTarget defaultValue:0 menuEntries:@[@"目标 1",@"目标 2",@"目标 3"] parameterFlags:kFxParameterFlag_NOT_ANIMATABLE|kFxParameterFlag_HIDDEN]&&ok;
ok=[api addPushButtonWithName:@"向前分析" parameterID:PGAnalyzeForward selector:@selector(analyzeForward) parameterFlags:0]&&ok; ok=[api addPushButtonWithName:@"向前分析" parameterID:PGAnalyzeForward selector:@selector(analyzeForward) parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_DISABLED]&&ok;
ok=[api addPushButtonWithName:@"向后分析" parameterID:PGAnalyzeBackward selector:@selector(analyzeBackward) parameterFlags:0]&&ok; ok=[api addPushButtonWithName:@"向后分析" parameterID:PGAnalyzeBackward selector:@selector(analyzeBackward) parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_DISABLED]&&ok;
ok=[api addPushButtonWithName:@"清除当前目标" parameterID:PGClearTrack selector:@selector(clearTracking) parameterFlags:0]&&ok; ok=[api addPushButtonWithName:@"清除当前目标" parameterID:PGClearTrack selector:@selector(clearTracking) parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_DISABLED]&&ok;
ok=[api addStringParameterWithName:@"跟踪状态" parameterID:PGStatus defaultValue:@"选择画面中的人脸后开始分析" parameterFlags:kFxParameterFlag_NOT_ANIMATABLE|kFxParameterFlag_DISABLED]&&ok; ok=[api addStringParameterWithName:@"跟踪状态" parameterID:PGStatus defaultValue:@"自动人脸跟随" parameterFlags:kFxParameterFlag_NOT_ANIMATABLE|kFxParameterFlag_DISABLED|kFxParameterFlag_HIDDEN]&&ok;
toggle(@"显示人脸框",PGFaceBoxes,NO); toggle(@"显示跟踪框",PGTrackBoxes,NO); end(); toggle(@"显示人脸框",PGFaceBoxes,NO); toggle(@"显示跟踪框",PGTrackBoxes,NO); end();
group(@"蒙版与输出",105); slider(PGThreshold); slider(PGSoftness); slider(PGEdgeProtect); group(@"蒙版与输出",105); slider(PGThreshold); slider(PGSoftness); slider(PGEdgeProtect);
toggle(@"预览蒙版",PGShowMask,NO); toggle(@"反转蒙版",PGInvertMask,NO); toggle(@"预览蒙版",PGShowMask,NO); toggle(@"反转蒙版",PGInvertMask,NO);
ok=[api addPopupMenuWithName:@"蒙版通道" parameterID:PGDebugChannel defaultValue:0 menuEntries:@[@"皮肤",@"牙齿",@"眼睛"] parameterFlags:0]&&ok; ok=[api addPopupMenuWithName:@"蒙版通道" parameterID:PGDebugChannel defaultValue:0 menuEntries:@[@"皮肤",@"牙齿",@"眼睛"] parameterFlags:kFxParameterFlag_HIDDEN]&&ok;
slider(PGMix); end(); slider(PGMix); end();
for(UInt32 identifier=PGTrackData1;identifier<=PGTrackData3;identifier++) for(UInt32 identifier=PGTrackData1;identifier<=PGTrackData3;identifier++)
ok=[api addStringParameterWithName:@"分析数据" parameterID:identifier defaultValue:@"" parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_NOT_ANIMATABLE]&&ok; ok=[api addStringParameterWithName:@"分析数据" parameterID:identifier defaultValue:@"" parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_NOT_ANIMATABLE]&&ok;
return ok ? YES : PGFail(error,@"宿主未能创建完整的人像美化参数。"); return ok ? YES : PGFail(error,@"宿主未能创建完整的人像美化参数。");
} }
- (NSView *)createViewForParameterID:(UInt32)identifier {
if(identifier!=PGInspectorPanel)return nil;
PGInspectorView *view=[[PGInspectorView alloc] initWithAPIManager:_apiManager];
@synchronized(_inspectorViews){[_inspectorViews addObject:view];}return view;
}
- (BOOL)parameterChanged:(UInt32)identifier atTime:(CMTime)time error:(NSError **)error {
// The host callback covers undo and playhead-driven parameter changes. Hop
// to AppKit's thread and avoid retaining remote inspector views in render.
NSArray *views;@synchronized(_inspectorViews){views=_inspectorViews.allObjects;}
for(PGInspectorView *view in views)dispatch_async(dispatch_get_main_queue(), ^{[view hostParameterChanged];});
return YES;
}
- (Class)classForCustomParameterID:(UInt32)identifier {return identifier==PGInspectorPanel?PGInspectorState.class:Nil;}
- (NSSet<Class> *)classesForCustomParameterID:(UInt32)identifier {
return identifier==PGInspectorPanel?[NSSet setWithObjects:PGInspectorState.class,NSDictionary.class,NSNumber.class,NSSet.class,nil]:[NSSet set];
}
- (PGBeautySettings)settingsAtTime:(CMTime)time { - (PGBeautySettings)settingsAtTime:(CMTime)time {
PGBeautySettings s=PGBeautyDefaults(); PGBeautySettings s=PGBeautyDefaults();
id<FxParameterRetrievalAPI_v6> api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]; id<FxParameterRetrievalAPI_v6> api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
@@ -177,6 +214,13 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
enabled=YES; [api getBoolValue:&enabled fromParameter:PGShapeEnabled atTime:time]; enabled=YES; [api getBoolValue:&enabled fromParameter:PGShapeEnabled atTime:time];
if(!enabled) s.faceThin=s.faceBigEye=s.faceShort=s.faceNarrow=s.faceChin=s.faceV=s.faceJaw=s.faceCheekbone=s.faceSmallHead=0; if(!enabled) s.faceThin=s.faceBigEye=s.faceShort=s.faceNarrow=s.faceChin=s.faceV=s.faceJaw=s.faceCheekbone=s.faceSmallHead=0;
if(s.fastMultiFace) s.singleFace=0; if(s.fastMultiFace) s.singleFace=0;
NSObject *panel=nil;
[api getCustomParameterValue:&panel fromParameter:PGInspectorPanel atTime:time];
if([panel isKindOfClass:PGInspectorState.class])s=PGSettingsWithInspectorState(s,(PGInspectorState *)panel);
// The current product follows faces automatically. Old manual-analysis and
// debug values remain saved for document compatibility, but cannot silently
// override live geometry or leave an invisible mask/selection mode active.
s.trackingEnabled=0;s.singleFace=0;s.showMask=0;s.invertMask=0;s.debugChannel=0;s.showFaceBoxes=0;
return s; return s;
} }
- (NSArray<NSString *> *)trackStrings { - (NSArray<NSString *> *)trackStrings {
@@ -192,7 +236,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
if(!state) return PGFail(error,@"缺少渲染状态输出位置。"); if(!state) return PGFail(error,@"缺少渲染状态输出位置。");
if(![_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]) return PGFail(error,@"无法读取宿主参数。"); if(![_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]) return PGFail(error,@"无法读取宿主参数。");
PGBeautySettings s=[self settingsAtTime:time]; PGBeautySettings s=[self settingsAtTime:time];
*state=[NSPropertyListSerialization dataWithPropertyList:@{@"settings":[NSData dataWithBytes:&s length:sizeof(s)],@"tracks":[self trackStrings]} format:NSPropertyListBinaryFormat_v1_0 options:0 error:error]; *state=[NSPropertyListSerialization dataWithPropertyList:@{@"settings":[NSData dataWithBytes:&s length:sizeof(s)],@"tracks":@[@"",@"",@""]} format:NSPropertyListBinaryFormat_v1_0 options:0 error:error];
return *state!=nil; return *state!=nil;
} }
- (BOOL)destinationImageRect:(FxRect *)rect sourceImages:(NSArray<FxImageTile *> *)sources destinationImage:(FxImageTile *)destination pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error { - (BOOL)destinationImageRect:(FxRect *)rect sourceImages:(NSArray<FxImageTile *> *)sources destinationImage:(FxImageTile *)destination pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error {
@@ -222,6 +266,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
if(![plist isKindOfClass:NSDictionary.class]||![plist[@"settings"] isKindOfClass:NSData.class]||[plist[@"settings"] length]!=sizeof(PGBeautySettings)) return PGFail(error,@"渲染状态版本无效。"); if(![plist isKindOfClass:NSDictionary.class]||![plist[@"settings"] isKindOfClass:NSData.class]||[plist[@"settings"] length]!=sizeof(PGBeautySettings)) return PGFail(error,@"渲染状态版本无效。");
PGBeautySettings s;[plist[@"settings"] getBytes:&s length:sizeof(s)]; PGBeautySettings s;[plist[@"settings"] getBytes:&s length:sizeof(s)];
if(s.version!=1) return PGFail(error,@"此版本不支持保存的渲染状态。"); if(s.version!=1) return PGFail(error,@"此版本不支持保存的渲染状态。");
s.trackingEnabled=0;s.singleFace=0;
CIImage *input=PGImageFromTile(sources.firstObject); if(!input) return PGFail(error,@"无法读取 IOSurface。"); CIImage *input=PGImageFromTile(sources.firstObject); if(!input) return PGFail(error,@"无法读取 IOSurface。");
@synchronized(self) { @synchronized(self) {
if(!_reportedRender) { if(!_reportedRender) {
@@ -239,13 +284,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
@synchronized(_faceProcessor) { @synchronized(_faceProcessor) {
NSArray *tracks=plist[@"tracks"]; NSArray *tracks=plist[@"tracks"];
if(![tracks isKindOfClass:NSArray.class]||tracks.count!=3) return PGFail(error,@"跟踪状态无效。"); if(![tracks isKindOfClass:NSArray.class]||tracks.count!=3) return PGFail(error,@"跟踪状态无效。");
if(![_loadedTracks isEqualToArray:tracks]) { for(id track in tracks) if(![track isKindOfClass:NSString.class]) return PGFail(error,@"跟踪记录无效。");
for(NSUInteger i=0;i<3;i++) {
if(![tracks[i] isKindOfClass:NSString.class]) return PGFail(error,@"跟踪记录无效。");
[_faceProcessor setTrackingData:[[NSData alloc] initWithBase64EncodedString:tracks[i] options:0] forTarget:i];
}
_loadedTracks=[tracks copy];
}
NSError *stageError=nil; NSError *stageError=nil;
PGFaceFrame *frame=[_faceProcessor processImage:input settings:s time:time error:&stageError]; PGFaceFrame *frame=[_faceProcessor processImage:input settings:s time:time error:&stageError];
if(!frame) { os_log_error(PGRenderLog(), "face processing failed: %{public}@",stageError); if(error)*error=stageError; return NO; } if(!frame) { os_log_error(PGRenderLog(), "face processing failed: %{public}@",stageError); if(error)*error=stageError; return NO; }
@@ -263,7 +302,6 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
FxRect b=destination.tilePixelBounds; FxRect b=destination.tilePixelBounds;
output=[output imageByApplyingTransform:CGAffineTransformMakeTranslation(-b.left,-b.bottom)]; output=[output imageByApplyingTransform:CGAffineTransformMakeTranslation(-b.left,-b.bottom)];
size_t width=IOSurfaceGetWidth((__bridge IOSurfaceRef)destination.ioSurface),height=IOSurfaceGetHeight((__bridge IOSurfaceRef)destination.ioSurface); size_t width=IOSurfaceGetWidth((__bridge IOSurfaceRef)destination.ioSurface),height=IOSurfaceGetHeight((__bridge IOSurfaceRef)destination.ioSurface);
if(destination.imageOrigin==kFxImageOrigin_TOP_LEFT) output=[output imageByApplyingTransform:CGAffineTransformMake(1,0,0,-1,0,height)];
@try {[context render:output toIOSurface:(__bridge IOSurfaceRef)destination.ioSurface bounds:CGRectMake(0,0,width,height) colorSpace:nil];} @try {[context render:output toIOSurface:(__bridge IOSurfaceRef)destination.ioSurface bounds:CGRectMake(0,0,width,height) colorSpace:nil];}
@catch(NSException *exception) {return PGFail(error,exception.reason?:@"图像处理失败。");} @catch(NSException *exception) {return PGFail(error,exception.reason?:@"图像处理失败。");}
return YES; return YES;
@@ -287,7 +325,8 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
// A separate processor owns the mutable analysis session. Render snapshots // A separate processor owns the mutable analysis session. Render snapshots
// cannot overwrite records being accumulated by the analyzer. // cannot overwrite records being accumulated by the analyzer.
_analysisFaceProcessor=[PGFaceProcessor new]; _analysisFaceProcessor=[PGFaceProcessor new];
[_analysisFaceProcessor setTrackingData:[[NSData alloc] initWithBase64EncodedString:tracks[s.targetIndex] options:0] forTarget:s.targetIndex]; NSData *previous=[[NSData alloc] initWithBase64EncodedString:tracks[s.targetIndex] options:0];
[_analysisFaceProcessor setTrackingData:PGCompatibleHostTrack(previous)?previous:nil forTarget:s.targetIndex];
_analysisSettings=s;_analysisSettings.trackingEnabled=1;_analysisSettings.singleFace=1; _analysisSettings=s;_analysisSettings.trackingEnabled=1;_analysisSettings.singleFace=1;
_analysisForward=forward;_analyzing=YES; _analysisForward=forward;_analyzing=YES;
BOOL ok=forward?[api startForwardAnalysis:kFxAnalysisLocation_CPU error:&error]:[api startBackwardAnalysis:kFxAnalysisLocation_CPU error:&error]; BOOL ok=forward?[api startForwardAnalysis:kFxAnalysisLocation_CPU error:&error]:[api startBackwardAnalysis:kFxAnalysisLocation_CPU error:&error];
@@ -332,6 +371,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
id<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)]; id<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
NSData *data=nil; NSData *data=nil;
@synchronized(_analysisFaceProcessor) {data=[_analysisFaceProcessor trackingDataForTarget:_analysisSettings.targetIndex];} @synchronized(_analysisFaceProcessor) {data=[_analysisFaceProcessor trackingDataForTarget:_analysisSettings.targetIndex];}
data=PGMarkHostTrack(data);
@synchronized(_faceProcessor) {_loadedTracks=nil;} @synchronized(_faceProcessor) {_loadedTracks=nil;}
if(data.length>64*1024*1024) { if(data.length>64*1024*1024) {
_analyzing=NO; _analyzing=NO;
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.4</string> <string>1.1.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>6</string> <string>9</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string> <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
@@ -71,3 +71,11 @@ REA 直接打开内嵌插件。原包为 arm64/x86_64 通用 Mach-O,Ghidra 无
本机原生观察:Motion中使用同一蓝发视频,原版UI默认多人关闭(单人)、防跳180%;启用检测框并调瘦脸/大眼后,播放时自动更新人脸框,没有先执行离线分析。此观察只覆盖该单人片段,不能替代多人遮挡与所有参数对照。 本机原生观察:Motion中使用同一蓝发视频,原版UI默认多人关闭(单人)、防跳180%;启用检测框并调瘦脸/大眼后,播放时自动更新人脸框,没有先执行离线分析。此观察只覆盖该单人片段,不能替代多人遮挡与所有参数对照。
1.0.4已在Motion实际加载与不同播放头位置观察默认自动跟随输出;用户随后接管Motion/FCP自测,FCP该版自动跟随及显式OSC原生点击尚未完成自动验收。已完成范围与剩余验证见 [验证记录](../验证记录.md)。 1.0.4已在Motion实际加载与不同播放头位置观察默认自动跟随输出;用户随后接管Motion/FCP自测,FCP该版自动跟随及显式OSC原生点击尚未完成自动验收。已完成范围与剩余验证见 [验证记录](../验证记录.md)。
## 最大瘦脸与转脸差异(用户复测反馈)
用户反馈跟踪正常,但最大瘦脸与原版不一致,侧脸出现异常变形。补充REA与Metal AIR分析确认,原版瘦脸以76点加中心点输入,执行17组轮廓到鼻部/瞳孔锚点的局部逆向采样;当前复刻采用矩形框水平对称场,两者没有对齐。已检查的GPU瘦脸路径没有显式yaw角度切换或侧脸衰减,不能将局部二维点自适应描述为完整3D姿态识别。详见 [脸型变形与转脸分析](脸型变形与转脸分析.md) 及 [结构报告](slim-pose-report.json)。本轮未修改安装版的变形代码。
## 1.0.5 对齐实现
已根据上述证据将瘦脸本身改为76点/17组局部逆向采样,`.025 × sizeGain`映射、UV距离、每组clamp和顺序均由独立数值场对照。没有复制原版metallib或模型。全76点参与当前实时稳定并保存离线记录;缺失完整点阵时不再退回整框瘦脸。其他8类几何控制、完整77点稳定系数和快速多人重叠采样仍有既有差异。阶段验证见验证记录;原分析中“1.0.4尚未修复”是历史状态。
+75
查看文件
@@ -0,0 +1,75 @@
{
"date": "2026-10-21",
"target_sha256": "f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb",
"claim_type": "static CPU assembly/constants and Metal AIR reconstruction, independently tested; no native-host observation",
"chin_pins": [
[
68,
38
],
[
67,
37
],
[
66,
36
]
],
"chin_parameter_index": 5,
"signed_slider_range": [
-1,
1
],
"positive_scale": 0.04,
"negative_scale": 0.02,
"cpu_evidence": [
"ev_640ccb714c5ce00d3864844af319fc574d223810dde08443d2ef2e8f1e989c47",
"ev_8bc31392c07788d2482546121297c3f177d527282275ef21fd3e5edef9921544"
],
"air_sha256": "f97705b88f5491057df2117aef457ffca84e11020574cd57742b0670db4484d8",
"air_file_local_ignored": "build/pose-analysis/face-thin.air.txt",
"coordinate_contract": "Vision normalized y-up; upload pins and sampling coordinate converted once to y-down UV, result returned to CI y-up",
"gain": "clamp(280/max(length((P75-P59)*imageSize),2*length((P67-(P74+P60)/2)*imageSize),1),1,2.25)",
"pin_formula": "q=clamp(q-(anchor-origin)*ratio*clamp(1-distance(q,origin)/distance(anchor,origin),0,1),0,1)",
"positive_visual_direction": "chin toward lower lip (shorter)",
"negative_visual_direction": "chin away from lower lip (longer)",
"verification": {
"beauty": 49,
"face": 116,
"thin_chin": 394,
"failures": 0,
"maximum_uv_oracle_error": 1.09828022e-05,
"realVideoASanChecks": 494,
"realVideoFrames": 11,
"independentAnatomicalChecks": 12,
"fullChecks": 904,
"hostAdapterASanChecks": 247,
"oscASanChecks": 98,
"allOriginTagsPixelIdentical": true,
"maxByteErrorVsIndependentCIReference": 2,
"oldFlipNegativeControl": {
"checks": 120,
"expectedFailures": 30,
"topTopDefaultMaxByteError": 0,
"topTopBigEyeMaxByteError": 66,
"topTopChinPositiveMaxByteError": 52,
"topTopChinNegativeMaxByteError": 21,
"scope": "temporary current source with only two old flips restored; three-pin chin already fixed; not same as old Gaussian eye-hotspot counterexample"
}
},
"limitations": [
"Seven other shape controls remain approximations.",
"Combined analytic controls and multi-face resampling order is not fully reference equivalent.",
"Motion/FCP final visual validation is reserved for user."
],
"adapterCorrection": {
"change": "remove imageOrigin-based input/output flips to match reference CI/IOSurface path",
"legacyTrackPolicy": "preserve unmarked strings but use realtime follow; reanalysis writes hostCoordinateSpace=iosurface-ci-y-up-v1",
"referenceOutputEvidence": [
"ev_88a71ec62fade296495f1e3f5428cdb15ad4cda43866e09fb040fa3a4bdb0440",
"ev_110d76f7958decb0c56b7daafe292d60a8bc56cd035b40a997f61395cb91ebfe"
],
"nativeBufferCaptured": false
}
}
+1
查看文件
@@ -0,0 +1 @@
{"artifacts":[{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o"}],"captures":[],"environments":[],"providers":[{"id":"ghidra","name":"Ghidra","version":"12.1.4"}],"records":[{"analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"authority":"shipped-artifact","confidence":"observed","environment":null,"evidence_id":"ev_640ccb714c5ce00d3864844af319fc574d223810dde08443d2ef2e8f1e989c47","evidence_links":[],"limitations":["The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.","Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use <percent-encoded-space>:0x<hex>.","Bytes reflect Ghidra's initialized memory, including loader fixups. Reads stop at an unmapped or uninitialized byte and report completeness explicitly.","macOS sessions require a matching executable Ghidra native decompiler; REA checks for it but does not build native components or change Gatekeeper quarantine state."],"locations":[],"normalized_result":{"address":"0x10005f9c0","bytes_hex":"7b14ae47e17a943f7b14ae47e17aa43f","complete":true,"requested_bytes":16,"returned_bytes":16},"operation":"read_bytes","parameters":{"address":"0x10005f9c0","length":16},"predicate_type":"rea.analysis","provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"},"raw_result":{"address":"0x10005f9c0","bytes_hex":"7b14ae47e17a943f7b14ae47e17aa43f","complete":true,"requested_bytes":16,"returned_bytes":16},"subject":{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o","local_path":"/private/tmp/paintfcp-rea.b3j9Te/MSE18V2-arm64","name":"MSE18V2-arm64"}},{"analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"authority":"shipped-artifact","confidence":"observed","environment":null,"evidence_id":"ev_8bc31392c07788d2482546121297c3f177d527282275ef21fd3e5edef9921544","evidence_links":[],"limitations":["The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","Cancelling an active Ghidra request terminates its ephemeral database and discards Line truncated
文件差异过大,未显示。 加载差异
+1
查看文件
@@ -0,0 +1 @@
{"artifacts":[{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o"}],"captures":[],"environments":[],"providers":[{"id":"ghidra","name":"Ghidra","version":"12.1.4"}],"records":[{"analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"authority":"shipped-artifact","confidence":"observed","environment":null,"evidence_id":"ev_02f734a0d72f1b7b77a3112d1de297b9e99ea43d5377349e6eebb71ab017d650","evidence_links":[],"limitations":["The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.","Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use <percent-encoded-space>:0x<hex>.","Pseudocode is Ghidra decompiler output, not original source and not text-equivalent to Hopper output.","External functions and functions without an analyzable body return null; other decompiler failures remain explicit.","Ghidra Listing reports a terminal call at 0x100016b84 to 0x10005c880 objc_stub::setUsesCPUOnly: (external=false, thunk=false, hasNoReturn=false); fallthrough is excluded by Ghidra's flow model. The reason for the terminal flow is not established by this observation.","macOS sessions require a matching executable Ghidra native decompiler; REA checks for it but does not build native components or change Gatekeeper quarantine state."],"locations":[],"normalized_result":"\nvoid FUN_100016a3c(ID param_1,ID param_2,ID param_3)\n\n{\n undefined8 uVar1;\n int iVar2;\n ID IVar3;\n ID IVar4;\n ID IVar5;\n \n if (param_1 == 0) {\n return;\n }\n if ((param_2 == 0) || (IVar3 = objc_stub::revision(param_2), IVar3 == 0)) {\n IVar5 = VNDetectFaceLandmarksRequest::supportedRevisions\n ((ID)&_OBJC_CLASS___VNDetectFaceLandmarksRequest);\n IVar3 = 3;\n IVar4 = VNDetectFaceLandmarksRequest::containsIndex_(IVar5,3);\n if ((IVar4 & 1) == 0) {\n IVar5 = VNDetectFaceLandmarksRequest::containsIndex_(IVar5,2);\n IVar3 = 1;\n if ((int)IVar5 != 0) {\n IVar3 = 2;\n }\n }\n }\n else {\n IVar3 = objc_stub::revision(param_2);\n }\n IVar5 = VNDetectFaceLandmarksRequest::supportedRevisions\n ((ID)&_OBJC_CLASS___VNDetectFaceLandmarksRequest);\n IVar5 = VNDetectFaceLandmarksRequest::containsIndex_(IVar5,IVar3);\n if ((IVar5 & 1) == 0) {\n IVar3 = VNDetectFaceLandmarksRequest::supportedRevisions\n ((ID)&_OBJC_CLASS___VNDetectFaceLandmarksRequest);\n IVar5 = VNDetectFaceLandmarksRequest::containsIndex_(IVar3,3);\n if ((int)IVar5 == 0) {\n IVar3 = VNDetectFaceLandmarksRequest::containsIndex_(IVar3,2);\n uVar1 = 1;\n if ((int)IVar3 != 0) {\n uVar1 = 2;\n }\n objc_stub::setRevision_(param_1,uVar1);\n goto LAB_100016b4c;\n }\n objc_stub::setRevision_(param_1,3);\n }\n else {\n objc_stub::setRevision_(param_1,IVar3);\n if (IVar3 != 3) goto LAB_100016b4c;\n }\n IVar3 = VNDetectFaceLandmarksRequest::revision_supportsConstellation_\n ((ID)&_OBJC_CLASS___VNDetectFaceLandmarksRequest,3,2);\n if ((int)IVar3 != 0) {\n objc_stub::setConstellation_(param_1,2);\n }\nLAB_100016b4c:\n IVar3 = objc_stub::count(param_3);\n iVar2 = 0;\n if (IVar3 != 0) {\n IVar3 = objc_stub::setInputFaceObservations_(param_1,param_3);\n iVar2 = (int)IVar3;\n }\n FUN_10003b084();\n if (iVar2 == 0) {\n return;\n }\n objc_stub::setUsesCPUOnly_(param_1,1Line truncated
+401
查看文件
@@ -0,0 +1,401 @@
{
"schema": 1,
"date": "2026-10-21",
"request": "最大瘦脸差异与人物转脸时的变形机制",
"reference": {
"plugin_sha256": "6c5d74bc844a5c5f745718b48ea10ec6635d08aa08a5b6dc6e62653980849f8a",
"arm64_sha256": "f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb",
"metallib_sha256": "4e8c77db8be94ae169337a2e646407f69520b63afb248faa653e0e068237aaae"
},
"air_observation": {
"authority": "static Apple metal-objdump AIR disassembly; not observed shader execution",
"command": "xcrun metal-objdump --metallib --disassemble-symbols=faceThinFragmentShader <reference default.metallib>",
"full_output_sha256": "f97705b88f5491057df2117aef457ffca84e11020574cd57742b0670db4484d8",
"full_output_bytes": 274496,
"local_output": "build/pose-analysis/face-thin.air.txt",
"module": "faceThinFragmentShader",
"module_metallib_offset": "0x5d5e",
"function": "mssFaceShapePinCoordinate",
"active_branch": "faceShapeParamsActive selects Shape helper; inactive branch alone selects legacy Thin helper",
"local_pin_triplets": [
[
75,
74,
6
],
[
59,
60,
13
],
[
74,
73,
47
],
[
60,
61,
47
],
[
73,
72,
47
],
[
61,
62,
47
],
[
72,
71,
48
],
[
62,
63,
48
],
[
71,
70,
48
],
[
63,
64,
48
],
[
70,
69,
49
],
[
64,
65,
49
],
[
69,
68,
49
],
[
65,
66,
49
],
[
68,
67,
52
],
[
66,
67,
52
],
[
67,
65,
47
]
],
"triplet_usage": "first=origin, third=anchor; second unused in thin loop",
"thin_cpu_scale": 0.025,
"size_gain": "clamp(280/max(length((P75-P59)*imageSize),2*length((P67-(P74+P60)/2)*imageSize),1),1,2.25)",
"effective_pin_gain": "clamp(UI thin,0,1) * strengthMultiplier * 0.025 * size_gain",
"radius": "length(origin-anchor) in normalized image UV",
"falloff": "clamp(1-length(currentSampleCoordinate-origin)/radius,0,1)",
"inverse_sample_step": "q = clamp(q-(anchor-origin)*effective_pin_gain*falloff,0,1)",
"iteration": "17 sequential updates; not 17 independent offsets evaluated at one original coordinate",
"bounded_negative_findings": [
"No yaw/roll/pitch input to the checked GPU helpers",
"No explicit side-view angle gate in the checked thin branch",
"No explicit eye/nose restore mask in the checked thin branch; each single pin has zero weight at its own anchor"
],
"limits": [
"This is a 2D local landmark deformation, not proof of 3D head pose estimation",
"Static paths are not a capture of the exact runtime branch used in the supplied screenshots",
"Other contour controls include fixed horizontal offsets; not all controls share rotation-equivariant behavior"
]
},
"vision_probe": {
"authority": "Independent public Apple Vision / AVFoundation invocation; no reference app or plug-in executed",
"script": "scripts/inspect-face-geometry.m",
"revision": 3,
"constellation": 76,
"maximum_dimension": 1280,
"coordinate_system": "full-image normalized, y up",
"region_indices": {
"contour": [
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75
],
"innerLips": [
42,
40,
43,
45,
41,
44
],
"leftEye": [
0,
4,
5,
1,
3,
2
],
"leftPupil": [
6
],
"nose": [
47,
56,
54,
53,
52,
51,
50,
55
],
"noseCrest": [
46,
47,
48,
49,
58,
57
],
"outerLips": [
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
26
],
"rightEye": [
7,
11,
12,
8,
10,
9
],
"rightPupil": [
13
]
},
"frames": [
{
"time": 0,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 2,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0.7853981852531433
],
"pitch_radians": [
null
]
},
{
"time": 4,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
1.5707963705062866
],
"pitch_radians": [
null
]
},
{
"time": 6,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 8,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 10,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 12,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 14,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 16,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 18,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
1.5707963705062866
],
"pitch_radians": [
null
]
},
{
"time": 20,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0.7853981852531433
],
"pitch_radians": [
null
]
}
],
"result": "All 11 sampled frames returned one face and 76 points; 4s/18s reported yaw approx pi/2; pitch unavailable",
"privacy": "Source frames, complete landmarks and source path remain ignored under build/pose-analysis/vision and are not included in Git"
},
"reconstruction_baseline": {
"commit": "e03e460a9b3e5ad489aad24095485ba10ceb987d",
"installed_version": "1.0.4 (6)",
"root_causes": [
"thin deformation uses bounding box x/y/size rather than saved contour/nose points",
"horizontal symmetric inverse warp is applied at full strength for any valid box",
"no contour/landmark completeness guard or pose metadata preservation",
"fixed coefficients do not match reference local-pin scaling"
],
"direction_check": "Positive thin samples outward and moves image features inward; the sign is not reversed",
"test_gap": "Existing geometry tests establish pixel changes, extent and persistence, not reference likeness or profile shape preservation"
},
"checklist": {
"why_maximum_slim_differs": "answered: different geometric model, locality and effective strength",
"how_profile_is_handled": "answered within checked active thin path: projected 2D landmarks, local directions/radii; no established explicit yaw gate",
"full_native_visual_equivalence": "unverified; screenshots are not a synchronized isolated-parameter capture",
"product_fix": "not changed by this analysis; installed 1.0.4 retains the reported issue"
},
"host_boundary": "User controls Motion/FCP; no UI interaction, host shutdown, app install or plug-in replacement in this analysis.",
"followup_implementation": {
"version": "1.0.5 (7)",
"signed_source_commit": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964",
"status": "signed_notarized_installed_user_visual_qa_pending",
"landmarks": "explicit revision3 / 76 constellation, points76 persisted and stabilized",
"warp": "17 sequential UV pins, .025 * pixel-size gain, original thin bbox term removed",
"offline_checks": 604,
"extra_real_video_asan_checks": 264,
"real_video_frames": 11,
"max_uv_oracle_error": 1.09828022e-05,
"limitations": [
"Other eight controls retain prior approximations",
"Fast multi-face overlapping resampling order is not equivalent to the original single fragment",
"Full native Motion/FCP visual comparison reserved for user"
]
}
}
+42
查看文件
@@ -0,0 +1,42 @@
# 下巴控制与图像坐标核对
2026-10-21,接用户反馈“调整下巴却调整眼睛”。本轮不操作、启动或退出Motion/FCP。以已保存的原版AIR和新REA CPU汇编/常量证据继续分析;没有执行原版或复制其二进制到产品。
## 已确认的下巴差异
参数发布、PGChin=24、settings.faceChin与GPU参数位置没有串到大眼。1.0.5的下巴仍用脸框高斯场,存在两项错误:作用范围延伸到嘴部甚至眼睑,下巴正负方向与原版相反。独立真实照片上,旧公式的最大位移出现在嘴附近;侧脸真实下巴位移接近零。这些结论来自离屏照片/坐标场探针,不能代替实际宿主当帧诊断。
原版`0x1000279f4`汇编将下巴滑块约束到[-1,1],正值乘0.04、负值乘0.02,写入脸型结构第5项(byte offset20);常量位于`0x10005f9c0`。REA记录:`ev_8bc31392c07788d2482546121297c3f177d527282275ef21fd3e5edef9921544`与`ev_640ccb714c5ce00d3864844af319fc574d223810dde08443d2ef2e8f1e989c47`,完整小型证据包见[rea-chin.json](rea-chin.json)。原件身份与上一轮一致,arm64切片SHA-256为`f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb`。
`mssFaceShapePinCoordinate`的AIR分支读取第5项及共同尺寸增益,顺序执行68→38、67→37、66→36三组局部采样位移。独立Vision区域核对:66/67/68在下巴轮廓,36/37/38在下唇。其半径、线性衰减、逐步clamp与17组瘦脸相同;支持负值。视觉正值缩短下巴、负值拉长,方向随下巴→嘴唇向量变化,不固定沿画面Y轴。细节和边界见[机器报告](chin-report.json)。
## 修正与测试
已移除旧下巴高斯项,改用原版三组点、正负系数、尺寸增益。完整76点缺失或不合法时跳过下巴,避免旧档案使用猜测框位置;其他七类脸型控制仍是现有近似实现,组合顺序未宣称全部恢复。
核心回归559项通过:美化49、人脸116、瘦脸/下巴394。新增正负下巴、半强度、左右侧脸投影、歪头、三种宽高比、非零原点、瘦脸叠加、坏点与旧档案保护。独立double采样场最大UV误差1.09828022e-5。真实NASA人像额外检查下巴位移、正负向量符号和两侧瞳孔保持不动;以坐标位移判定,避免只检查“像素有改变”。
## 图像适配层待核对项
纯CI图像中,76点落在真实瞳孔和下巴,没有全局Y轴反置。但FxPlug适配层曾根据imageOrigin在输入、输出各翻转一次。以相同物理top-first照片字节离屏构造TOP_LEFT输入输出时,旧代码默认直通仍精确一致,却能复现下巴作用到眼部的现象;这说明直通测试无法排除中间处理倒置。
imageOrigin描述坐标轴方向,不能单凭枚举推断IOSurface的物理行序。原版render主入口包装CIImage后仅见正尺度和位移,仍需核对其输出helper,不能仅凭主函数的有界检索宣称原版整个链条无翻转。原生FCP本次实际缓冲行序未捕获,最后视觉交由用户自测。临时照片、完整人脸坐标和探针输出均留在被忽略的build目录。
## 原版输出链闭环与适配修复
继续REA分析两个输出helper后,确认主入口所见的无额外翻转不是仅靠字符串缺失作出的猜测:
- `0x10002911c`直接调用`CIContext render:toIOSurface:bounds:colorSpace:`,没有修改图像方向。证据`ev_88a71ec62fade296495f1e3f5428cdb15ad4cda43866e09fb040fa3a4bdb0440`。
- `0x1000291b4`备用路径先render到BGRA8/RGBAh/RGBAf位图,再按源、目的行步长同时正向递增复制到IOSurface,没有倒行。证据`ev_110d76f7958decb0c56b7daafe292d60a8bc56cd035b40a997f61395cb91ebfe`,另有汇编交叉核对。完整记录见[rea-image-orientation.json](rea-image-orientation.json)。
已移除我们输入/输出两处基于TOP_LEFT的额外翻转,保持与原版同类CI→IOSurface路径一致。独立探针在相同物理top-first字节下验证默认/大眼/下巴正负×四种源/目的标签共16组,全部与upright CI参考逐字节一致;旧代码在相同构造下出现下巴到眼部的热点,且默认直通0误差不能揭示内部倒置。这里验证的是物理缓冲构造与修复链,未捕获本次实际FCP缓冲。
Apple[FxImageOrigin](https://developer.apple.com/documentation/professional_video_applications/fximageorigin)和本机SDK说明其描述坐标轴方向;[FxPlug渲染说明](https://developer.apple.com/documentation/professional-video-applications/rendering-in-fxplug)确认FxPlug4经IOSurface传入图像。文档本身未规定每个宿主的物理行序,本次改动依据为原版链和独立物理测试,不能把文档扩张成原生内存捕获。
旧档案没有记录图像适配坐标契约,无法安全判断是否由旧翻转链产生。adapter现为新分析结果加`hostCoordinateSpace=iosurface-ci-y-up-v1`;未标记记录保留原字符串,渲染和续分析均不载入旧坐标。当前目标暂用实时跟随,新分析后恢复离线记录。不改通用PGFaceProcessor的旧档案读取能力,也不把用户记录直接删除或静默改写成新坐标。
独立生理热点检查:NASA、实际素材0秒、4秒侧脸三张图均在真实下巴1.2–2.9像素内获得最大位移,实际下巴正值上收、负值延伸,双瞳孔采样位移不超过0.00003像素;12项专项检查通过。另同一素材11帧共494项瘦脸/下巴ASan检查通过,含重复基础用例,不与完整回归简单相加。用户照片、帧及完整点位均没有入库。
## 新回归的负控
在临时副本只恢复旧的输入/输出翻转、保留已修正三pin后,120项解剖子测试产生30个预期失败:混合原点标签出现方向错误,TOP/TOP默认仍0误差,但大眼最大差66/255、下巴正值52/255、负值21/255,均被独立参考和跨标签逐字节一致性检出。此时三pin下巴热点仍在下巴,因为点驱动形变可随图像反射;“眼部热点”是此前旧高斯下巴与额外翻转共同构成的反例,二者不能混为同一代码状态。正式当前实现247项宿主适配ASan检查全部通过。
@@ -0,0 +1,86 @@
# 最大瘦脸与转脸变形分析
2026-10-21。用户已反馈自动跟随正常;本次检查四张实测图指出的脸型差异。当前安装版仍为1.0.4(6),本次没有修改产品变形代码、替换APP或操作Motion/FCP。
## 结论
我们的瘦脸按矩形脸框建立左右对称的水平收缩场;原版主要瘦脸路径根据完整二维面部关键点,顺序执行17组局部位移。两者的作用范围、方向和强度定义均不同。侧脸继续使用正面矩形场,是当前鼻尖、下巴和邻近区域异常变形的直接原因;跟踪框跟对人不能解决这个问题。
原版已检查的瘦脸路径没有读取yaw/roll/pitch后切换侧脸算法,也没有专门的侧脸角度衰减。它每帧获得随姿态变化的二维轮廓与鼻部位置,让局部收缩方向和作用半径随这些点变化。这属于二维关键点自适应,不能表述成已恢复完整3D姿态估计。
## 原版证据链
只读目标与上一轮一致:插件原件SHA-256为`6c5d74bc844a5c5f745718b48ea10ec6635d08aa08a5b6dc6e62653980849f8a`;arm64分析切片为`f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb`。本次开始后重新计算原件摘要,未发生变化。Metal库摘要为`4e8c77db8be94ae169337a2e646407f69520b63afb248faa653e0e068237aaae`。
|步骤|直接静态观察|REA Evidence|
|---|---|---|
|检测设置|`0x100016a3c`优先配置Vision revision 3及76点constellation;不支持时存在旧revision回退|`ev_02f734a0d72f1b7b77a3112d1de297b9e99ea43d5377349e6eebb71ab017d650`|
|当前帧检测|`cachedFaceObservations…`发起`VNDetectFaceLandmarksRequest`,另有重试与缓存|`ev_61ea8c6e7117f18e47fe6cf820c9ec0a59865dedaf3d9c09813be0a5bd7a79bc`|
|有效点筛选|`0x1000230cc`要求至少76点;`0x100022c70`转换76点并添加第77个中心点;`0x100017cd8`检查有限数值、空间范围及轮廓分布|`ev_63bb7c8fb07dbe456027f4d4e6be52118338b91ab39dbf9ed6620de9090bc260`、`ev_5f27f174c793c94cd45219d6c405e6b54b96e2650c7462ccc5eeaea14f2604c3`、`ev_1c62b4dab3cf8b661eb5c31b034a0b3cadf9f1f6cb512aace8b3b0838bc74b5c`|
|输入几何|`0x1000274d0`打包77个float2,616字节,转换到输出纹理坐标并检查越界点/轮廓尺度|`ev_8a33a7bb27a1a966f9e4ee26380501f1cb009d7d4665ed93b73bd3f630436556`|
|参数缩放|`0x1000279f4`将瘦脸滑块值乘0.025及渲染强度,写入十float脸型结构的第一项|`ev_449e1fc8883b8afe27240f672f98f300d4132ac4994db5e1590a4f7ca4e21278`|
|GPU连接|`FxMTLDeviceCacheItem::initWithDevice:pixelFormat:`将`faceThinFragmentShader`接入瘦脸pipeline;`0x100027d38/0x10002807c`上传点阵、数量及脸型参数到该pipeline|`ev_1520c2d83845004994f2be940c8c5ea3f4a62daf2d8e3e06278f381ffca1a010`、`ev_dc4487de4891c4a663dab370f2e207799bec7e2ad305a98a1ed579e5de6ef263`、`ev_7d2cc9ff3bf8ed3ceea2d91ffb7c2c47ad45337a792f101c1d989272f37023b1`|
完整REA记录见[证据包](rea-slim-pose.json)。伪代码是Ghidra生成的静态分析结果,寄存器参数与CGRect返回类型部分恢复不完整;此处没有将其当作原始源码或实际执行记录。
## Metal瘦脸计算
用本机Apple工具对只读`default.metallib`输出AIR中间代码。它不是字符串猜测:`faceThinFragmentShader`在`faceShapeParamsActive`为真时调用`mssFaceShapePinCoordinate`,只有inactive分支调用旧`mssFaceThinPinCoordinate`。开启瘦脸后第一项脸型参数非零,因此可建立Shape分支的静态路径。原版截图当时具体走哪条宿主渲染分支没有运行捕获。
Shape helper中的瘦脸部分有17组点操作,每组实际读取origin和anchor,三元表中的第二项在这一循环中未使用。origin均为59–75号脸轮廓点,anchor为6、13、47、48、49、52号点;独立Vision区域对应检查确认,6/13是左右瞳孔,47/48/49/52位于鼻部或鼻梁。不能把所有锚点都叫下颌点或鼻部点。
单组逆向采样的结构为:
```text
r = distance(origin, anchor)
w = clamp(1 - distance(q, origin) / r, 0, 1)
q = clamp(q - (anchor - origin) * effectiveStrength * w, 0, 1)
```
17组顺序更新同一个采样坐标q,不能将其视为一次整脸等比缩放。这里的距离在整幅纹理归一化UV中计算;它并非完整按像素各向同性的旋转不变模型。单组在其anchor处权重为零,但其他组仍可能影响该处,不能说整个鼻部或所有五官被锁死。
有效强度另含尺寸补偿:`k=clamp(280/max(轮廓两端像素距离, 下巴到上侧轮廓中点的像素距离×2, 1), 1, 2.25)`,单pin系数约为`滑块×0.025×k×渲染强度`。它对小脸加大效果,**不是侧脸质量衰减**。最大单组系数0.05625也不是最终整脸缩窄百分比。
AIR结构摘要、全部17组索引、尺寸公式、输出摘要和复现命令在[机器可读报告](slim-pose-report.json)。完整AIR文本留在本地`build/pose-analysis/face-thin.air.txt`,未把原版metallib或原版程序加入产品或仓库。
## 为什么转脸时更自然
已证实的机制是:轮廓投影变化→局部origin、anchor间向量变化→各处的收缩方向、半径随之变化;并非先证明“人物转头90度”,再换一套固定对称瘦脸公式。另有上一轮已分析的时间点阵稳定、有效性检查和失检参考保持,这些可减少跳动;它们与瘦脸位移模型是两个层面。
本次检查的GPU瘦脸分支没有yaw/roll/pitch输入、左右脸对称比门限、侧脸强度开关或末端五官恢复蒙版。原生函数名检索也未发现`objc_stub::yaw/roll/pitch` getter;这是有界负面结果,不能据此宣称整个APP绝无任何姿态相关逻辑。其他脸型项仍有固定画面X方向位移,不能把瘦脸的局部点特性扩张成所有控制都具有相同的转头适应性。
## 同一素材的独立验证
运行独立的[离线检测工具](../../scripts/inspect-face-geometry.m),从用户视频在0、2、4、6、8、10、12、14、16、18、20秒各取一帧。请求使用Vision revision 3 / 76点,图像最长边1280。全部11帧均返回1张脸、76点;4秒和18秒的Vision yaw约为π/2,pitch均不可用。
这证明明显侧脸仍可以获得完整二维点阵,也校验了59–75号点确为轮廓、6/13号为瞳孔、47/48/49/52号为鼻部相关点。yaw为检测器返回值,不能当作实测头部角度。工具没有调用或执行原版滤镜,不是原版宿主像素等价测试;本机Vision版本、检测尺寸与宿主缓存可能使点坐标与原版当帧不同。Vision本身报告yaw,也不能说明原版瘦脸实际使用该属性。
用户素材、截图、导出的帧及完整人脸坐标均只保留在被忽略的本地build目录,没有上传远程仓库。入库的报告仅含汇总计数、区域索引和角度信息。
工具复现:
```sh
xcrun clang -fobjc-arc -Wall -Wextra \
-framework Foundation -framework AVFoundation -framework Vision \
-framework ImageIO -framework CoreGraphics -framework CoreMedia \
scripts/inspect-face-geometry.m -o /tmp/inspect-face-geometry
/tmp/inspect-face-geometry '<用户视频路径>' '<本地输出目录>/geometry.json' 0 2 4 6 8 10 12 14 16 18 20
```
## 当前实现差异与修复落点
`PGFaceProcessor.m:290–305`的瘦脸只用框中心/宽高,按画面水平轴同时处理左右;`warpImage:geometry:settings:extent:`额外读取的双眼仅用于大眼。已有`contour/nose/lips`未参与瘦脸,缺失轮廓时仍会按有效框足额变形。pose信息也没有贯通提取、净化、实时稳定和离线存储。
正值采样向外、视觉向内收的符号本身正确;简单反号无法修复。当前公式在框高35%处的测试位置将脸框两边缘间距缩小约20.2%,作用域还可延伸到框外。这个局部计算是当前公式的解析检查,不代表与原版截图的实测宽度差。
后续修正应让瘦脸直接使用稳定后的轮廓和鼻部控制点,按局部方向/半径位移,并校准尺寸补偿和滑块映射。若另加显式姿态或质量衰减,应标记为本项目设计,不能当作已证实的原版算法。完整点阵必须贯通实时与持久化路径,不能只在检测入口新增字段后被后续步骤丢弃。
现有测试只能证明各几何参数改变像素、范围保持和跟踪恢复一致,尚未验证最大瘦脸外观或侧脸形状。修复验证至少应包括同帧正面/半侧/全侧、向左右转脸、歪头、轮廓缺失、正逆播放、100%瘦脸与多个脸型项叠加,和原版在隔离相同其他参数后的对照。
本轮完成机制分析及独立检测,未宣称已修复安装版,也未将用户的“跟踪没问题”扩张为其他未覆盖的原生验收通过。
## 后续修复状态
1.0.5已实现并安装76点/17组局部瘦脸,旧的矩形框瘦脸已移除。上文“安装版未修复”为本次分析开始时1.0.4的历史状态。新增独立数值场与真实素材回归,签名公证及安装记录见 `../验证记录.md`。Motion/FCP最终视觉由用户自测,其他8类脸型控制与快速多人重叠的既有近似边界仍保留。
1.0.6继续分析下巴与宿主图像轴,新增原版三pin下巴及CI/IOSurface无额外翻转修复,详见[下巴与坐标分析](下巴与坐标分析.md)。其余七类控制仍为近似实现。
+5
查看文件
@@ -0,0 +1,5 @@
Beauty renderer: 49 checks, 0 failures (Metal).
2026-10-21 20:18:32.623 face-tests[6394:5408005] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
Face processing: 116 checks passed
2026-10-21 20:18:36.942 slim-tests[6433:5408137] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
PASS: 224 slim alignment checks; maximum UV oracle error 1.09828022e-05
@@ -0,0 +1,49 @@
{
"status": "installed_user_qa_pending",
"bundleIdentifier": "com.dayu.beatygril",
"installationPath": "/Library/Plug-Ins/FxPlug/beatygril.app",
"options": {
"waitForHosts": true,
"userTests": true,
"resumePrepared": true
},
"nativeEffectQA": {
"status": "pending_user",
"performedByScript": false
},
"source": {
"commit": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964",
"trackedWorktreeChanges": []
},
"updatedAt": "2026-10-21T12:23:23.275614+00:00",
"version": "1.0.5",
"build": "7",
"stagingSignature": "Executable=/private/tmp/paintfcp-build-20261021/Build/Products/Release/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518\nCandidateCDHashFull sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720\nHash choices=sha256\nCMSDigest=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720\nCMSDigestType=2\nCDHash=573debde15f2628c7bc7c6ed9a4cdeabd33f7518\nSignature size=8968\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:20:14\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n",
"notarization": {
"id": "7d7034f8-7389-410f-ad8e-9e722408c0fc",
"status": "Accepted",
"message": "Processing complete"
},
"resumedBy": {
"commit": "a6551c334fd1a0883259700b802174955c78376a",
"trackedWorktreeChanges": []
},
"signingVerified": true,
"stapleValidated": true,
"stagingPluginUnregistered": true,
"runningHosts": [],
"installedSignature": "Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518\nCandidateCDHashFull sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720\nHash choices=sha256\nCMSDigest=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720\nCMSDigestType=2\nCDHash=573debde15f2628c7bc7c6ed9a4cdeabd33f7518\nSignature size=8968\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:20:14\nNotarization Ticket=stapled\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n",
"installedSignatureVerified": true,
"installedStapleValidated": true,
"gatekeeperAccepted": true,
"pluginDiscovery": {
"bundleIdentifier": "com.dayu.beatygril.fxplug",
"formalPath": "/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit",
"output": " com.dayu.beatygril.fxplug(1.1)\tF419A538-F395-459D-9984-AC46EAB5E83D\t2026-10-21 12:23:23 +0000\t/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit\n (1 plug-in)\n"
},
"removedGeneratedPaths": [
"/Users/dayu/Documents/PaintFCP/build/delivery/previous-beatygril.app",
"/tmp/paintfcp-build-20261021",
"/Users/dayu/Documents/PaintFCP/build/delivery/notarization.zip"
]
}
+4
查看文件
@@ -0,0 +1,4 @@
2026-10-21 20:18:33.844 host-tests[6427:5408121] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
Default IOSurface maximum byte difference: 0
Alternate destination tag maximum byte difference: 0
Host adapter: 117 checks, 0 failures
@@ -0,0 +1,2 @@
com.dayu.beatygril.fxplug(1.1) F419A538-F395-459D-9984-AC46EAB5E83D 2026-10-21 12:23:23 +0000 /Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit
(1 plug-in)
@@ -0,0 +1,24 @@
Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril
Identifier=com.dayu.beatygril
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518
CandidateCDHashFull sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720
Hash choices=sha256
CMSDigest=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720
CMSDigestType=2
CDHash=573debde15f2628c7bc7c6ed9a4cdeabd33f7518
Signature size=8968
Authority=Developer ID Application: zhong luo (B3W25FX5H5)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Oct 21, 2026 at 20:20:14
Notarization Ticket=stapled
Info.plist entries=22
TeamIdentifier=B3W25FX5H5
Runtime Version=27.0.0
Sealed Resources version=2 rules=13 files=4
Internal requirements count=1 size=180
Total signatures=1
Chosen signature=1
@@ -0,0 +1,32 @@
{
"version": "1.0.5",
"build": "7",
"uiOperations": 0,
"template": {
"path": "/Users/dayu/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/大鱼人像美化.moef",
"sha256": "ba08de6916cd837b3733aa0c6fcb78f2e8224431d25e1310089ae67d18445f2d",
"resourceVerified": true
},
"codesign": "",
"xcrun": "Processing: /Library/Plug-Ins/FxPlug/beatygril.app\nThe validate action worked!",
"spctl": "/Library/Plug-Ins/FxPlug/beatygril.app: accepted\nsource=Notarized Developer ID",
"Contents/MacOS/beatygril": "x86_64 arm64",
"Contents/PlugIns/beatygril XPC Service.pluginkit/Contents/MacOS/beatygril XPC Service": "x86_64 arm64",
"duplicateInventory": {
"scopes": [
"/Users/dayu/Documents/PaintFCP",
"/Users/dayu/Library/Developer/Xcode/DerivedData",
"/Users/dayu/Desktop",
"/Users/dayu/Downloads",
"/Applications",
"/Library/Plug-Ins/FxPlug",
"/Users/dayu/Library/Plug-Ins/FxPlug"
],
"matchingOuterApps": [
"/Library/Plug-Ins/FxPlug/beatygril.app"
],
"errors": []
},
"discovery": " com.dayu.beatygril.fxplug(1.1)\tF419A538-F395-459D-9984-AC46EAB5E83D\t2026-10-21 12:23:23 +0000\t/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit\n (1 plug-in)\n",
"status": "installed_verified_user_visual_tests_pending"
}
@@ -0,0 +1 @@
{"id":"7d7034f8-7389-410f-ad8e-9e722408c0fc","status":"Accepted","message":"Processing complete"}
+21
查看文件
@@ -0,0 +1,21 @@
2026-10-21 20:18:34.451 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
2026-10-21 20:18:34.466 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,180) surface=320x240 origin=0
2026-10-21 20:18:34.897 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.8,0.3) setter=(1152,-432) readback=(0.8,0.3) mode=centered-pixels verified=1
2026-10-21 20:18:34.897 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.7,0.4) objectBounds=(-1920,-1080,3840,2160) canvas=(152,78) surface=320x240 origin=0
2026-10-21 20:18:34.948 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.8,0.3) setter=(0.8,0.3) readback=(0.12345,0.87654) mode=normalized verified=0
2026-10-21 20:18:34.948 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.75,0.25) setter=(0.75,0.25) readback=(0.75,0.25) mode=normalized verified=1
2026-10-21 20:18:34.948 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.75,0.25) objectBounds=(-100,-50,200,100) canvas=(160,60) surface=320x240 origin=0
2026-10-21 20:18:35.005 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.625,0.375) setter=(0.625,0.375) readback=(0.625,0.375) mode=normalized verified=1
2026-10-21 20:18:35.005 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.625,0.375) objectBounds=(-100,-50,200,100) canvas=(240,60) surface=320x240 origin=0
2026-10-21 20:18:35.057 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
2026-10-21 20:18:35.057 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(144,180) surface=320x240 origin=0
2026-10-21 20:18:35.108 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 20:18:35.108 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0,1) objectBounds=(-100,-50,200,100) canvas=(100,140) surface=320x240 origin=0
2026-10-21 20:18:35.167 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(nan,nan) surface=320x240 origin=0
2026-10-21 20:18:35.217 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 20:18:35.217 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(inf,nan) surface=320x240 origin=0
2026-10-21 20:18:35.267 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 20:18:35.267 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,nan) surface=320x240 origin=0
2026-10-21 20:18:35.316 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 20:18:35.316 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
OSC adapter: 98 checks, 0 failures
@@ -0,0 +1,11 @@
Independent remote recovery test
Target: abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964
Command: zsh scripts/test.sh
Exit code: 0
Beauty renderer: 49 checks, 0 failures (Metal).
Face processing: 116 checks passed
PASS: 224 slim alignment checks; maximum UV oracle error 1.09828022e-05
2026-10-21 20:20:38.246 face-tests[7122:5413459] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
2026-10-21 20:20:40.227 slim-tests[7129:5413512] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
@@ -0,0 +1,740 @@
{
"target": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964",
"remote": "https://dayu.media/git/dayu/paintfcp-fxplug.git",
"startedAt": "2026-10-21T12:20:32.822716+00:00",
"temporaryClone": "/var/folders/t4/kbc10sqs43b0rjf3nfh51nmw0000gn/T/paintfcp-recovery-abe97ed-um2yk1vm/clone",
"scope": "Independent source recovery plus scripts/test.sh only; no APP build, host execution/UI, installation, root source changes, commit, host/OSC ASan or delivery-lock operation.",
"cloneHead": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964",
"files": {
"count": 69,
"allByteBlobAndModeEqual": true,
"entries": [
{
"path": ".gitignore",
"mode": "100644",
"kind": "blob",
"blob": "d2bca6e38076bb5f7574cc7781eadef27b41d9ff",
"bytes": 490,
"sha256": "34e63260d343569ad4d5a1ffd6c15647da5a7da2b8940dc93f6d987c7fec474e",
"byteBlobAndModeEqual": true
},
{
"path": "README.md",
"mode": "100644",
"kind": "blob",
"blob": "d0282233a2a630f2a00a7bd1e3342ba4f3a0791a",
"bytes": 3167,
"sha256": "3f1d5c727a335ef862837db92a57ba684424e802cbdee4d64f6bdae069e43076",
"byteBlobAndModeEqual": true
},
{
"path": "RECOVERY.md",
"mode": "100644",
"kind": "blob",
"blob": "d97c7731d0a3d349c72982ad156a581ef6a03a87",
"bytes": 4183,
"sha256": "2f310d4b5f4df318b8b522963c3f1bbe35c1e2a3b3ed85a112d59fc10205c36a",
"byteBlobAndModeEqual": true
},
{
"path": "Templates/大鱼人像美化/Drop Zone.tiff",
"mode": "100644",
"kind": "blob",
"blob": "6d15c98bc221f999514f197ff67a7145836e6a38",
"bytes": 420128,
"sha256": "a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e",
"byteBlobAndModeEqual": true
},
{
"path": "Templates/大鱼人像美化/PARAMETERS.json",
"mode": "100644",
"kind": "blob",
"blob": "df2c01f370e6ee639d2950774fe8020742802c17",
"bytes": 9815,
"sha256": "bb561b0dfff90223f0925a60ec05b28518e22163a6bf7922c4efc6d155ddbabb",
"byteBlobAndModeEqual": true
},
{
"path": "Templates/大鱼人像美化/README.md",
"mode": "100644",
"kind": "blob",
"blob": "46cf59f6b4500b32c5a203a63a79a491a2202cdf",
"bytes": 4420,
"sha256": "c57ef81f43271d566a0488385d5f6b8e492142bae0860049bcbf943206b0c567",
"byteBlobAndModeEqual": true
},
{
"path": "Templates/大鱼人像美化/VALIDATION.json",
"mode": "100644",
"kind": "blob",
"blob": "63aa4f2ce4e1e72ce8b255d5f98fe209e2354c09",
"bytes": 1872,
"sha256": "558ac5ff3bee28e6d3df41cc5ffd9a43cd51fca2fd001fff8641260c327e351d",
"byteBlobAndModeEqual": true
},
{
"path": "Templates/大鱼人像美化/大鱼人像美化.moef",
"mode": "100644",
"kind": "blob",
"blob": "f68569a62b399cf7ad1ee2b33427372fc9b6b913",
"bytes": 22996,
"sha256": "ba08de6916cd837b3733aa0c6fcb78f2e8224431d25e1310089ae67d18445f2d",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril.xcodeproj/project.pbxproj",
"mode": "100644",
"kind": "blob",
"blob": "db5251bf3c337b865621e771a4b28690214f1c8e",
"bytes": 22986,
"sha256": "e9b9be100e53003f6bba96658d3a36078bb607236958e43aec6ed59f8c73ce32",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"mode": "100644",
"kind": "blob",
"blob": "919434a6254f0e9651f402737811be6634a03e9c",
"bytes": 135,
"sha256": "7f3b00b5c3fdb45242d7b87e1e5c4e25d1fa8129a16c94295ecc4e8ea2235c5f",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/Info.plist",
"mode": "100644",
"kind": "blob",
"blob": "95542fa8c27e6770b9266389f0aa34027053e31f",
"bytes": 3030,
"sha256": "865f1775fb476ff1f22556141b41b25d3569c05e4aa1d2450cccdcb06de6aa4c",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGBeautyRenderer.h",
"mode": "100644",
"kind": "blob",
"blob": "a0a6965c837072e6d837a98b701d59b1cc139655",
"bytes": 1410,
"sha256": "4e9b663c47261e4feceb7ab38c25171b65f36c14417d82a8b7c49ee3201492d3",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGBeautyRenderer.m",
"mode": "100644",
"kind": "blob",
"blob": "4c9fc6fbb1dc1f2c4048a16435f554cae9f2ca74",
"bytes": 17132,
"sha256": "00b52b1f22c1e875f137ee4ea90f50d6caa38db277bebd0da538d0ba7e66072b",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGBeautyTypes.h",
"mode": "100644",
"kind": "blob",
"blob": "e1cd3ef21f37b831cdaa7d9f52211cafaf243994",
"bytes": 1149,
"sha256": "6ea8005accc22f85fe8be1b73bf6e89a5b37ffd6e97460d4f6dd69d55eaf33cc",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGFaceProcessor.h",
"mode": "100644",
"kind": "blob",
"blob": "0b861830a6e9652d81ccfe62f2ce758febee449f",
"bytes": 2178,
"sha256": "a3448bd75fd278c9702f079f55637ba2c26bc820bdbb81f49fb3a3d0e2a4aba5",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGFaceProcessor.m",
"mode": "100644",
"kind": "blob",
"blob": "08c1e85455da07203e4ee30a5410fb10cb9e78c7",
"bytes": 42670,
"sha256": "445aac56a8ff7332c3b94c6ad5b37b9e412af757995a8332ae2e70066d348d7e",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGFaceSelectOSC.m",
"mode": "100644",
"kind": "blob",
"blob": "ae0aa7ce1c2de355b2c1cb890314a0de689df81a",
"bytes": 11630,
"sha256": "d5e1dd1db6825730803ddc3f21b588cd548cbbacc60760aa45ad9e2feedae307",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGParameterIDs.h",
"mode": "100644",
"kind": "blob",
"blob": "4cc0e20af1e659f1e8e666fabe01003c06cfd885",
"bytes": 932,
"sha256": "8fdfa2021131317ce09532d27fd6f34fb0b505f7b17c561819336ad2a06c42cc",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/beatygrilPlugIn.h",
"mode": "100644",
"kind": "blob",
"blob": "12dca07a744c064d5d474e0294304ca29055a077",
"bytes": 347,
"sha256": "4f00716328c9237b79cc58153ba47d8ac8ddc1786939cb206bd37b01e3b6397e",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/beatygrilPlugIn.m",
"mode": "100644",
"kind": "blob",
"blob": "3f148956b32770c7db5b0df97700fd96f7e20025",
"bytes": 24193,
"sha256": "b8f4326ed3eb2de74770e4ea266d09ebe4aea70428a4511f0ec501a0cd89b247",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/en.lproj/InfoPlist.strings",
"mode": "100644",
"kind": "blob",
"blob": "9a5f27348a39c8e3ee50cffc2fb359c78a1319c8",
"bytes": 85,
"sha256": "1b1cd5b74690bd5a74cf2b0a0c068bbc2c3462c5e9fe5ca3e61541bcbd451a7a",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/en.lproj/Localizable.strings",
"mode": "100644",
"kind": "blob",
"blob": "7f52f3dd719cb4f94e24867e36114cb061ef0b92",
"bytes": 132,
"sha256": "b3036a2339b97ebf264516e1e534351d1ceb64f45942595508f4d67d07b07769",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/main.m",
"mode": "100644",
"kind": "blob",
"blob": "34d50d3f8cdf438d3508d0aa94e3f2d20e45f962",
"bytes": 114,
"sha256": "5b812ddb1ef171fe075e4faf13d761935ca73ecbba1be0bd9d4802a64a7759cd",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/AppDelegate.h",
"mode": "100644",
"kind": "blob",
"blob": "f3f8e19cfe057114c2289ff37c01c2e6e1aa7ed0",
"bytes": 167,
"sha256": "3fc6516be93176f90c2deb17332c9cf98e670458a33fbad4fe7b8f367ec9494f",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/AppDelegate.m",
"mode": "100644",
"kind": "blob",
"blob": "1cb56f073356dd30add33415117bf4c24fdc4ef0",
"bytes": 3003,
"sha256": "acef6ade6591c039cd8b3e603f83d6d06f64da9709a0385b73c8588e824fe6de",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/Base.lproj/MainMenu.xib",
"mode": "100644",
"kind": "blob",
"blob": "57d03a50e249d189c5fa0a4602c5ec846f06612d",
"bytes": 50614,
"sha256": "7f0945bc5903a658550b712267870edecbdf1b73ac83c71f1358468f40c1ab1d",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/Info.plist",
"mode": "100644",
"kind": "blob",
"blob": "a1fe6cdcfe8e41f2de4ee07525e192e52723d760",
"bytes": 1084,
"sha256": "7b3ff213b68dda774306a96835cf4743e3998e23dd7ac4b20cbdb3cf709d7b8b",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/SandboxEntitlements.entitlements",
"mode": "100644",
"kind": "blob",
"blob": "852fa1a4728ae4789e3bca55dd07caef3b41f2a5",
"bytes": 240,
"sha256": "b746c74b201e6af4e0864fbd1f4f629f96156a7ec4b7208bc995d0a5089bb592",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/en.lproj/Credits.rtf",
"mode": "100644",
"kind": "blob",
"blob": "da779d0f6bec11568066e347a01ccef0308488b2",
"bytes": 264,
"sha256": "ec846e198df85720e8443abb810334411b5690386f3062f90208c23b8575f28a",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/en.lproj/InfoPlist.strings",
"mode": "100644",
"kind": "blob",
"blob": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/main.m",
"mode": "100644",
"kind": "blob",
"blob": "99ad4384792f9b2050e0755c07522c0dab9b4b86",
"bytes": 112,
"sha256": "5c3dc97a53d0379af0e0ce75c5541f33c47f2da115d2d0018cfba003d185a63d",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/REA-分析与复刻说明.md",
"mode": "100644",
"kind": "blob",
"blob": "6581570f9d140ef8d0e00f19d9285d44a6ae3813",
"bytes": 10519,
"sha256": "ee64e260fd9261856047096570b8fed308284b3a02cd0dd892f3ceda6a5482ab",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/auto-follow-report.json",
"mode": "100644",
"kind": "blob",
"blob": "e60d6d055a513ab8e9c21d2b39460c9d0698e1d3",
"bytes": 15665,
"sha256": "1a0c91efcaee058f331f6a7446bea8794af1dcf9120f5c2585f249b188d038e3",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/beauty-preview.png",
"mode": "100644",
"kind": "blob",
"blob": "35ace472cb42629b01b3e05660cb7be8b9711fd7",
"bytes": 617622,
"sha256": "37aa97855915a49ff1f225a9e1fa9d8bcbed0be5f4109b6835ba4db17f9113f6",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/face-preview.png",
"mode": "100644",
"kind": "blob",
"blob": "9a5bab3ad706b95ac32875352d16d6580dcd48ab",
"bytes": 713552,
"sha256": "6cc662cbb2b554a0dd8c926d94a0d819506ac76710d9c09c2d3d7f808fbf686b",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/rea-auto-tracking.json",
"mode": "100644",
"kind": "blob",
"blob": "2f5738f934f0ce89780cf4535c7257b54a397bef",
"bytes": 2883488,
"sha256": "dc1007f3798fade5e454bb2ef0b58e4cfe2bb0256948c364070fd9af70a97377",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/rea-host.json",
"mode": "100644",
"kind": "blob",
"blob": "9de443173a74668f0a2dced3426dae20549a16dd",
"bytes": 10111,
"sha256": "141866459948b19d5378de2f6534de333c795e317416f2ac6c9fdc86c94dd61a",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/rea-plugin.json",
"mode": "100644",
"kind": "blob",
"blob": "79bcafbcfeda694ed466d95f81f30eb622a95a0b",
"bytes": 343165,
"sha256": "dda6e450915e1054653c28addfed5254c678dccf14ee66643d508e7087c1f0c1",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/rea-slim-pose.json",
"mode": "100644",
"kind": "blob",
"blob": "249eb3678b20c378e0d73239e0949cc86c2af436",
"bytes": 343127,
"sha256": "5f4a2dfe5796ce72bb3d022a85a42cd5ed0eceb0cd0655041812126ebec7933a",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/reference-template-parameters.json",
"mode": "100644",
"kind": "blob",
"blob": "9947e2ff182a146bc4c9120dfff7eacc8cca9c0c",
"bytes": 1608,
"sha256": "026e47e8ecbd9a967d8b8aabddac2a370479a43b9e1a7a7728b5b01a5002f006",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/slim-pose-report.json",
"mode": "100644",
"kind": "blob",
"blob": "755e0dfcb6eef016e932415ad8c391b97f8de575",
"bytes": 8724,
"sha256": "512d22abc2b08860adb64c38efa0f8e3474b45e3dbd19cb81406b1d9e7060f06",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/脸型变形与转脸分析.md",
"mode": "100644",
"kind": "blob",
"blob": "b98271ebd17b745d2e16e6357578eac6a757c1c1",
"bytes": 8789,
"sha256": "0121d022e4ba430b9fd62b9a13da5b84d03866eb0d541e64e4da3204730c03cf",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/installed-signature.txt",
"mode": "100644",
"kind": "blob",
"blob": "78fc46fd6aee5443af9eeb53a97502d0251cf2a9",
"bytes": 988,
"sha256": "add93ef0fa60cd32ed766c965e6f2869f56b4da65030e774e76d133b02ef7aec",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/live-face-asan.txt",
"mode": "100644",
"kind": "blob",
"blob": "b92d565bec7a49444b638437b4ae8e7400ce90f6",
"bytes": 219,
"sha256": "b956e0fe7a3fccdd8eff8788807c0aaf60da56aa1cb153087f25234cc156c27d",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/notarization.json",
"mode": "100644",
"kind": "blob",
"blob": "86d17c6738b9e0aa6f92583ffc798c2e6b6bece1",
"bytes": 98,
"sha256": "91b4f5e239a50467bde83b8c40c1c1809f85cd4eb4b1f1091e1b2197c11a79b6",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/recovery-e03e460-beauty-face.txt",
"mode": "100644",
"kind": "blob",
"blob": "2c6f0b5e7f7dedf9571d6da6ce67303bf34faedc",
"bytes": 298,
"sha256": "45698c80fdfc68c2dc68be59ba03380aa6e8194731d4858dc009f827dd06e226",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/recovery-e03e460-host-asan.txt",
"mode": "100644",
"kind": "blob",
"blob": "7eeed088f0dad0564aa166232aedb9ae47a56dd0",
"bytes": 380,
"sha256": "65d0226f95e882702484849997fde558fae7af418e9243702f789b1e2fcacf14",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/recovery-e03e460-osc-asan.txt",
"mode": "100644",
"kind": "blob",
"blob": "7abde29290bfea1e475af387899fb3160a60cc95",
"bytes": 3336,
"sha256": "2e326d2fc0eb5d2cdd9f07e2318dfd17a41d346a82f383b05b51f416ffdacdb3",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/recovery-e03e460-xcode-list.txt",
"mode": "100644",
"kind": "blob",
"blob": "e90278ec1aedf6a1176682ec1037e49ba2f588c1",
"bytes": 570,
"sha256": "ff9ffb300caac840f4e64f97f24712e319af9bbe4cee1dbbd79b0a5caa1432d1",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/recovery-e03e460.json",
"mode": "100644",
"kind": "blob",
"blob": "ecd24b6f35c893c3637dc4e268d0acabb4d09967",
"bytes": 25360,
"sha256": "559839e989fb4fa9589847632975a6d4d8548c00e6ad5b87bdba736bb3e1daf3",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/recovery-e03e460.txt",
"mode": "100644",
"kind": "blob",
"blob": "ed81f4abdeada50c1afb2ac866d3cc79fd6579d9",
"bytes": 846,
"sha256": "3931beb81cf475ce1c44e5b90c8e9101abb1a7ba20b657d72090b6e7662530cc",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/release-receipt.json",
"mode": "100644",
"kind": "blob",
"blob": "e53f21de9e8ea68a40f435cf9e3d1a0d95499bde",
"bytes": 2454,
"sha256": "07ba4f5bbba101a49d4579c0b79fbce0dad70a1e4b5887e61e052741c49486ee",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/v1.0.5/core-tests.txt",
"mode": "100644",
"kind": "blob",
"blob": "76d29d91672ec631950b2f3ada1db39db4c877ac",
"bytes": 427,
"sha256": "408910aeedcf0155bc31ef43ca6e8994d9348f8fbdc0c25eb8a43ce89965e081",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/v1.0.5/host-tests.txt",
"mode": "100644",
"kind": "blob",
"blob": "3e1c1c6b7c104995a2656e142671759634352525",
"bytes": 271,
"sha256": "c727d9d56a0b46a4cdbc79d24eeedc589e5984b38144f28ccf017f4d65f504f7",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/v1.0.5/osc-tests.txt",
"mode": "100644",
"kind": "blob",
"blob": "c9c618896ca3ee0c2a943979103ab3b42335c7e0",
"bytes": 3229,
"sha256": "5e443009bc39635d01a1f6276b48170fe9da73c088646425c6ed140d87ba4518",
"byteBlobAndModeEqual": true
},
{
"path": "docs/验证记录.md",
"mode": "100644",
"kind": "blob",
"blob": "2d2d97b0e13e876bc8ddf65aedf671cbeead8b6b",
"bytes": 9154,
"sha256": "8ece9ba47c81980a781ab00b73c961bd32c1acec2e09811f348375e162e29661",
"byteBlobAndModeEqual": true
},
{
"path": "scripts/checkpoint.py",
"mode": "100644",
"kind": "blob",
"blob": "8248d1732aceed377d28cdae02873057f442474b",
"bytes": 2218,
"sha256": "3c27b0e603dd2d94512f611cec97bd9afe5809aea0f5a4bb213995d2e23366f8",
"byteBlobAndModeEqual": true
},
{
"path": "scripts/deliver.py",
"mode": "100755",
"kind": "blob",
"blob": "b20e2149f84307afd6198df71f967f4facb444bb",
"bytes": 10848,
"sha256": "dc5df519aaabc695adddff8e9087dcb6c6e9861036ab0efccf242a5fe12e0b78",
"byteBlobAndModeEqual": true
},
{
"path": "scripts/inspect-face-geometry.m",
"mode": "100644",
"kind": "blob",
"blob": "d075cf7c8c9a23b9bd01bbcbc31da89fea84d5d3",
"bytes": 6613,
"sha256": "a2c54e7297f9f94e9c56c0b29f9ce607756705f2005c1f13e10996bc26c24b66",
"byteBlobAndModeEqual": true
},
{
"path": "scripts/test-host.sh",
"mode": "100755",
"kind": "blob",
"blob": "ef9c7f6436e3d1babbf272e9d329d65ec6c395cb",
"bytes": 1129,
"sha256": "7f55ed6de05e6cc59884fb7c96062676bd086f4ac745ae847511eacb12316467",
"byteBlobAndModeEqual": true
},
{
"path": "scripts/test-osc.sh",
"mode": "100755",
"kind": "blob",
"blob": "ffaed0b8d449eef6acf2fb352d9fea10fc1649fd",
"bytes": 941,
"sha256": "ed405cd214ce252641074d0ef0a12c4cc0faf3e1869fe92716a98fff7f9ac517",
"byteBlobAndModeEqual": true
},
{
"path": "scripts/test.sh",
"mode": "100755",
"kind": "blob",
"blob": "f13ba656929fe34b43c143bdfeb4a208c44c7428",
"bytes": 932,
"sha256": "3ca9cb120b452a0c402debbe5d3399fa4996e0ea0882253470791494815d530d",
"byteBlobAndModeEqual": true
},
{
"path": "tests/beauty_render_tests.m",
"mode": "100644",
"kind": "blob",
"blob": "24050fb3a2630693253202ae1525c280f4f144eb",
"bytes": 16870,
"sha256": "b04eb2f0d78faec13bbdd2a3286cc74c46c2f536358c7c1c3d5d03c54daa868c",
"byteBlobAndModeEqual": true
},
{
"path": "tests/face_tests.m",
"mode": "100644",
"kind": "blob",
"blob": "9fe21ddd021d1a2517fa8b75d7a65532f6fefedb",
"bytes": 33180,
"sha256": "0350abe9f206fc371f697c993f5dea420d512259f16947c7a1167569843b215a",
"byteBlobAndModeEqual": true
},
{
"path": "tests/fixtures/ATTRIBUTION.txt",
"mode": "100644",
"kind": "blob",
"blob": "6e439ab97256952b0acd1eef0e1c1b4ab3ea015c",
"bytes": 498,
"sha256": "8823ad8aed1a5c1bdda93946e4b5ed24cd542e996a2b8087517cf62f7f4f2a84",
"byteBlobAndModeEqual": true
},
{
"path": "tests/fixtures/astronaut.png",
"mode": "100644",
"kind": "blob",
"blob": "834cda0012478c5edc8d43bade96d315dedeaab4",
"bytes": 791555,
"sha256": "88431cd9653ccd539741b555fb0a46b61558b301d4110412b5bc28b5e3ea6cb5",
"byteBlobAndModeEqual": true
},
{
"path": "tests/host_adapter_tests.m",
"mode": "100644",
"kind": "blob",
"blob": "8091c1523eb0b05295c391e20637c931d681e049",
"bytes": 30363,
"sha256": "77131c770222d3b404204fb04bf47cf943885eeff6acfc7498d59feb28eb1e73",
"byteBlobAndModeEqual": true
},
{
"path": "tests/osc_tests.m",
"mode": "100644",
"kind": "blob",
"blob": "66fff5f78f8e0ba08026e53375b184a460a29162",
"bytes": 22495,
"sha256": "a3199147e32facd63373b53f77840408411b04436fb1dd42e2fb71023c8f2f8a",
"byteBlobAndModeEqual": true
},
{
"path": "tests/slim_tests.m",
"mode": "100644",
"kind": "blob",
"blob": "3f7654f14e642f72ee57c2a5c3e92df39fe7346a",
"bytes": 12759,
"sha256": "701b52d7f803b44680586c215e5a351e4dc67661e5ea1a56ca5038567cd34cd6",
"byteBlobAndModeEqual": true
}
]
},
"requiredResources": [
{
"path": "Templates/大鱼人像美化/大鱼人像美化.moef",
"exists": true,
"bytes": 22996
},
{
"path": "Templates/大鱼人像美化/Drop Zone.tiff",
"exists": true,
"bytes": 420128
},
{
"path": "beatygril/beatygril/Wrapper Application/Base.lproj/MainMenu.xib",
"exists": true,
"bytes": 50614
},
{
"path": "beatygril/beatygril/Wrapper Application/en.lproj/Credits.rtf",
"exists": true,
"bytes": 264
},
{
"path": "beatygril/beatygril/Wrapper Application/en.lproj/InfoPlist.strings",
"exists": true,
"bytes": 0
},
{
"path": "beatygril/beatygril/Plugin/en.lproj/InfoPlist.strings",
"exists": true,
"bytes": 85
},
{
"path": "beatygril/beatygril/Plugin/en.lproj/Localizable.strings",
"exists": true,
"bytes": 132
},
{
"path": "tests/fixtures/astronaut.png",
"exists": true,
"bytes": 791555
},
{
"path": "tests/fixtures/ATTRIBUTION.txt",
"exists": true,
"bytes": 498
}
],
"gitFsck": {
"args": [
"git",
"fsck",
"--full",
"--strict"
],
"returncode": 0,
"stdout": "",
"stderr": ""
},
"submodules": {
"args": [
"git",
"submodule",
"status",
"--recursive"
],
"returncode": 0,
"stdout": "",
"stderr": ""
},
"lfsPointers": [],
"remoteMain": {
"args": [
"git",
"ls-remote",
"origin",
"refs/heads/main"
],
"returncode": 0,
"stdout": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964\trefs/heads/main\n",
"stderr": ""
},
"processorUnchangedFromEc22": {
"commit": "ec22f1b2cf541d02e7ffbd5cc4edbc4d0cd97311",
"equal": true
},
"test": {
"args": [
"zsh",
"scripts/test.sh"
],
"returncode": 0,
"stdout": "Beauty renderer: 49 checks, 0 failures (Metal).\nFace processing: 116 checks passed\nPASS: 224 slim alignment checks; maximum UV oracle error 1.09828022e-05\n",
"stderr": "2026-10-21 20:20:38.246 face-tests[7122:5413459] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2\n2026-10-21 20:20:40.227 slim-tests[7129:5413512] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2\n",
"elapsedSeconds": 3.384
},
"testCounts": {
"beauty": 49,
"face": 116,
"slim": 224
},
"testTotal": 389,
"cloneTrackedWorktreeClean": true,
"passed": true,
"temporaryCloneRemoved": true,
"completedAt": "2026-10-21T12:20:40.364542+00:00"
}
@@ -0,0 +1,9 @@
独立恢复验收:通过
源码节点:abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964
远程:https://dayu.media/git/dayu/paintfcp-fxplug.git
远程 main 与节点一致。
全部 69 个跟踪文件逐 byte/blob/模式一致;9 项必要资源完整。
git fsck --full --strict 通过;无 LFS 指针、无子模块。
独立 clone 内 scripts/test.sh:Beauty 49、Face 116、Slim 224;共 389 检查通过。
未重复 Host/OSC ASan;未构建 APP、启动宿主、操作 UI、安装、修改源码、提交或操作交付锁。
本次临时 clone 已清理。
@@ -0,0 +1,2 @@
2026-10-21 20:20:01.427 slim-asan[6677:5410585] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
PASS: 264 slim alignment checks; maximum UV oracle error 1.09828022e-05
@@ -0,0 +1,15 @@
{
"checks": 12,
"cases": 3,
"includes_profile": true,
"processor_sha256": "fe4f2a6fc28799cc9b81832baaa2d6739320bde9def175ecc8a4494d004d6b28",
"max_pupil_sampling_displacement_px": 2.8653327774463833e-05,
"peak_distance_to_real_chin_px": [
1.1827483260479077,
2.8140858161593
],
"positive_visual_direction": "toward lower lip",
"negative_visual_direction": "away from lower lip",
"actual_photos_and_full_geometry": "ignored local build only",
"scope": "standalone actual PNG anatomy; bypasses FxPlug adapter; no native host operation"
}
+5
查看文件
@@ -0,0 +1,5 @@
Beauty renderer: 49 checks, 0 failures (Metal).
2026-10-21 20:48:04.999 face-tests[9473:5455578] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
Face processing: 116 checks passed
2026-10-21 20:48:08.750 slim-tests[9480:5455684] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
PASS: 394 slim alignment checks; maximum UV oracle error 1.09828022e-05
@@ -0,0 +1,44 @@
{
"status": "installed_user_qa_pending",
"bundleIdentifier": "com.dayu.beatygril",
"installationPath": "/Library/Plug-Ins/FxPlug/beatygril.app",
"options": {
"waitForHosts": true,
"userTests": true
},
"nativeEffectQA": {
"status": "pending_user",
"performedByScript": false
},
"source": {
"commit": "ad54726b17fabd11f46bc6121e664eac38960a23",
"trackedWorktreeChanges": []
},
"updatedAt": "2026-10-21T13:53:56.294416+00:00",
"version": "1.0.6",
"build": "8",
"stagingSignature": "Executable=/private/tmp/paintfcp-build-20261021/Build/Products/Release/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8\nCandidateCDHashFull sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348\nHash choices=sha256\nCMSDigest=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348\nCMSDigestType=2\nCDHash=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8\nSignature size=8969\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:57:26\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n",
"notarization": {
"id": "ab0271c8-0c4b-4b10-9474-cebe9b55385b",
"message": "Processing complete",
"status": "Accepted"
},
"signingVerified": true,
"stapleValidated": true,
"stagingPluginUnregistered": true,
"runningHosts": [],
"installedSignature": "Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8\nCandidateCDHashFull sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348\nHash choices=sha256\nCMSDigest=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348\nCMSDigestType=2\nCDHash=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8\nSignature size=8969\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:57:26\nNotarization Ticket=stapled\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n",
"installedSignatureVerified": true,
"installedStapleValidated": true,
"gatekeeperAccepted": true,
"pluginDiscovery": {
"bundleIdentifier": "com.dayu.beatygril.fxplug",
"formalPath": "/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit",
"output": " com.dayu.beatygril.fxplug(1.1)\tA9F36A44-1E5E-4AB3-A2FD-5C66A76DB8BB\t2026-10-21 13:53:56 +0000\t/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit\n (1 plug-in)\n"
},
"removedGeneratedPaths": [
"/Users/dayu/Documents/PaintFCP/build/delivery/previous-beatygril.app",
"/tmp/paintfcp-build-20261021",
"/Users/dayu/Documents/PaintFCP/build/delivery/notarization.zip"
]
}
@@ -0,0 +1,48 @@
2026-10-21 20:56:23.542 negative-control[10726:5467718] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
FAIL: default sourceBOTTOM/destinationTOP matches independent upright CI output
FAIL: default sourceBOTTOM/destinationTOP is byte-identical across coordinate tags
FAIL: default sourceBOTTOM/destinationTOP preserves the independently decoded photo
FAIL: default sourceTOP/destinationBOTTOM matches independent upright CI output
FAIL: default sourceTOP/destinationBOTTOM is byte-identical across coordinate tags
FAIL: default sourceTOP/destinationBOTTOM preserves the independently decoded photo
FAIL: big-eye sourceBOTTOM/destinationTOP matches independent upright CI output
FAIL: big-eye sourceBOTTOM/destinationTOP is byte-identical across coordinate tags
FAIL: big-eye sourceBOTTOM/destinationTOP changes the two real eye regions
FAIL: big-eye sourceTOP/destinationBOTTOM matches independent upright CI output
FAIL: big-eye sourceTOP/destinationBOTTOM is byte-identical across coordinate tags
FAIL: big-eye sourceTOP/destinationBOTTOM changes the two real eye regions
FAIL: big-eye sourceTOP/destinationTOP matches independent upright CI output
FAIL: big-eye sourceTOP/destinationTOP is byte-identical across coordinate tags
FAIL: chin-positive sourceBOTTOM/destinationTOP matches independent upright CI output
FAIL: chin-positive sourceBOTTOM/destinationTOP is byte-identical across coordinate tags
FAIL: chin-positive sourceBOTTOM/destinationTOP changes the real chin rather than eyes or mirrored anatomy
FAIL: chin-positive sourceTOP/destinationBOTTOM matches independent upright CI output
FAIL: chin-positive sourceTOP/destinationBOTTOM is byte-identical across coordinate tags
FAIL: chin-positive sourceTOP/destinationBOTTOM changes the real chin rather than eyes or mirrored anatomy
FAIL: chin-positive sourceTOP/destinationTOP matches independent upright CI output
FAIL: chin-positive sourceTOP/destinationTOP is byte-identical across coordinate tags
FAIL: chin-negative sourceBOTTOM/destinationTOP matches independent upright CI output
FAIL: chin-negative sourceBOTTOM/destinationTOP is byte-identical across coordinate tags
FAIL: chin-negative sourceBOTTOM/destinationTOP changes the real chin rather than eyes or mirrored anatomy
FAIL: chin-negative sourceTOP/destinationBOTTOM matches independent upright CI output
FAIL: chin-negative sourceTOP/destinationBOTTOM is byte-identical across coordinate tags
FAIL: chin-negative sourceTOP/destinationBOTTOM changes the real chin rather than eyes or mirrored anatomy
FAIL: chin-negative sourceTOP/destinationTOP matches independent upright CI output
FAIL: chin-negative sourceTOP/destinationTOP is byte-identical across coordinate tags
Anatomy default 0/0 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
Anatomy default 0/1 max=255 mean=75.5590439 eye=512210/458308 chin=640530 inverted=2678038
Anatomy default 1/0 max=255 mean=75.5590439 eye=512210/458308 chin=640530 inverted=2678038
Anatomy default 1/1 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
Anatomy big-eye 0/0 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
Anatomy big-eye 0/1 max=255 mean=75.5509195 eye=512210/458308 chin=640530 inverted=2673702
Anatomy big-eye 1/0 max=255 mean=75.5510902 eye=512210/458308 chin=640530 inverted=2673881
Anatomy big-eye 1/1 max=66 mean=0.0180340 eye=30841/32051 chin=0 inverted=0
Anatomy chin-positive 0/0 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
Anatomy chin-positive 0/1 max=255 mean=75.5574808 eye=512210/458308 chin=640530 inverted=2671244
Anatomy chin-positive 1/0 max=255 mean=75.5593863 eye=512210/458308 chin=640530 inverted=2671753
Anatomy chin-positive 1/1 max=52 mean=0.0127611 eye=0/0 chin=121860 inverted=0
Anatomy chin-negative 0/0 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
Anatomy chin-negative 0/1 max=255 mean=75.5518637 eye=512210/458308 chin=640530 inverted=2681787
Anatomy chin-negative 1/0 max=255 mean=75.5510559 eye=512210/458308 chin=640530 inverted=2681767
Anatomy chin-negative 1/1 max=21 mean=0.0061512 eye=0/0 chin=63433 inverted=0
Restored origin flips negative control: 120 checks, 30 expected failures
+21
查看文件
@@ -0,0 +1,21 @@
2026-10-21 20:55:46.681 host-tests[10689:5467036] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
Default IOSurface maximum byte difference: 0
Alternate destination tag maximum byte difference: 0
Anatomy default 0/0 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
Anatomy default 0/1 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
Anatomy default 1/0 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
Anatomy default 1/1 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
Anatomy big-eye 0/0 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
Anatomy big-eye 0/1 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
Anatomy big-eye 1/0 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
Anatomy big-eye 1/1 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
Anatomy chin-positive 0/0 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
Anatomy chin-positive 0/1 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
Anatomy chin-positive 1/0 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
Anatomy chin-positive 1/1 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
Anatomy chin-negative 0/0 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
Anatomy chin-negative 0/1 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
Anatomy chin-negative 1/0 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
Anatomy chin-negative 1/1 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
Legacy fallback upright comparison: max=2 mean=0.0010357
Host adapter: 247 checks, 0 failures
@@ -0,0 +1,2 @@
com.dayu.beatygril.fxplug(1.1) A9F36A44-1E5E-4AB3-A2FD-5C66A76DB8BB 2026-10-21 13:53:56 +0000 /Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit
(1 plug-in)
@@ -0,0 +1,24 @@
Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril
Identifier=com.dayu.beatygril
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8
CandidateCDHashFull sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348
Hash choices=sha256
CMSDigest=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348
CMSDigestType=2
CDHash=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8
Signature size=8969
Authority=Developer ID Application: zhong luo (B3W25FX5H5)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Oct 21, 2026 at 20:57:26
Notarization Ticket=stapled
Info.plist entries=22
TeamIdentifier=B3W25FX5H5
Runtime Version=27.0.0
Sealed Resources version=2 rules=13 files=4
Internal requirements count=1 size=180
Total signatures=1
Chosen signature=1
@@ -0,0 +1,81 @@
{
"version": "1.0.6",
"build": "8",
"uiOperations": false,
"nativeVisualQA": "pending user",
"installationPath": "/Library/Plug-Ins/FxPlug/beatygril.app",
"codesign": {
"exitCode": 0,
"stdout": "",
"stderr": ""
},
"staple": {
"exitCode": 0,
"stdout": "Processing: /Library/Plug-Ins/FxPlug/beatygril.app\nThe validate action worked!",
"stderr": ""
},
"gatekeeper": {
"exitCode": 0,
"stdout": "",
"stderr": "/Library/Plug-Ins/FxPlug/beatygril.app: accepted\nsource=Notarized Developer ID"
},
"identity": [
{
"path": "/Library/Plug-Ins/FxPlug/beatygril.app",
"bundleIdentifier": "com.dayu.beatygril",
"version": "1.0.6",
"build": "8",
"archs": "x86_64 arm64",
"signature": "Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8\nCandidateCDHashFull sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348\nHash choices=sha256\nCMSDigest=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348\nCMSDigestType=2\nCDHash=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8\nSignature size=8969\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:57:26\nNotarization Ticket=stapled\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n"
},
{
"path": "/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit",
"bundleIdentifier": "com.dayu.beatygril.fxplug",
"version": "1.0.6",
"build": "8",
"archs": "x86_64 arm64",
"signature": "Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit/Contents/MacOS/beatygril XPC Service\nIdentifier=com.dayu.beatygril.fxplug\nFormat=bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=645 flags=0x10000(runtime) hashes=13+3 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=ded7ed887bb9c90fbf0c9e9b25a5b279c97e1da6\nCandidateCDHashFull sha256=ded7ed887bb9c90fbf0c9e9b25a5b279c97e1da6809e5a5bf42bd0dd56d42dfd\nHash choices=sha256\nCMSDigest=ded7ed887bb9c90fbf0c9e9b25a5b279c97e1da6809e5a5bf42bd0dd56d42dfd\nCMSDigestType=2\nCDHash=ded7ed887bb9c90fbf0c9e9b25a5b279c97e1da6\nSignature size=8969\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:57:25\nInfo.plist entries=26\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=188\nTotal signatures=1\nChosen signature=1\n"
}
],
"template": {
"previousTemplateSHA256": "ba08de6916cd837b3733aa0c6fcb78f2e8224431d25e1310089ae67d18445f2d",
"files": [
{
"path": "/Users/dayu/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/大鱼人像美化.moef",
"bytes": 22996,
"sha256": "30ca8edf36801264c5de2c753c40b720aba8052633620713a12401098fcd2409"
},
{
"path": "/Users/dayu/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/Drop Zone.tiff",
"bytes": 420128,
"sha256": "a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e"
}
],
"installedWithHostsClosed": true
},
"discovery": " com.dayu.beatygril.fxplug(1.1)\tA9F36A44-1E5E-4AB3-A2FD-5C66A76DB8BB\t2026-10-21 13:53:56 +0000\t/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit\n (1 plug-in)\n",
"duplicateInventory": {
"scopes": [
"/Users/dayu/Documents/PaintFCP",
"/Users/dayu/.codex/worktrees",
"/Users/dayu/Library/Developer/Xcode/DerivedData",
"/Users/dayu/Desktop",
"/Users/dayu/Downloads",
"/Applications",
"/Library/Plug-Ins/FxPlug",
"/Users/dayu/Library/Plug-Ins/FxPlug",
"/private/tmp/paintfcp-format-probe.PsxvYQ",
"/private/tmp/paintfcp-y-review.KpPi8T",
"/private/tmp/paintfcp-live-review.ItH4EG",
"/private/tmp/paintfcp-slim-field.6fDdJj",
"/private/tmp/paintfcp-chin-smoke.rfco16hq",
"/private/tmp/paintfcp-rea.b3j9Te",
"/private/tmp/paintfcp-slim-smoke.3ncdDe"
],
"matchingOuterApps": [
"/Library/Plug-Ins/FxPlug/beatygril.app"
],
"errors": []
},
"status": "installed, formal identity/ticket/discovery/template verified; user visual QA pending"
}
@@ -0,0 +1 @@
{"id":"ab0271c8-0c4b-4b10-9474-cebe9b55385b","message":"Processing complete","status":"Accepted"}
+21
查看文件
@@ -0,0 +1,21 @@
2026-10-21 20:53:14.697 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
2026-10-21 20:53:14.705 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,180) surface=320x240 origin=0
2026-10-21 20:53:15.118 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.8,0.3) setter=(1152,-432) readback=(0.8,0.3) mode=centered-pixels verified=1
2026-10-21 20:53:15.118 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.7,0.4) objectBounds=(-1920,-1080,3840,2160) canvas=(152,78) surface=320x240 origin=0
2026-10-21 20:53:15.168 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.8,0.3) setter=(0.8,0.3) readback=(0.12345,0.87654) mode=normalized verified=0
2026-10-21 20:53:15.168 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.75,0.25) setter=(0.75,0.25) readback=(0.75,0.25) mode=normalized verified=1
2026-10-21 20:53:15.168 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.75,0.25) objectBounds=(-100,-50,200,100) canvas=(160,60) surface=320x240 origin=0
2026-10-21 20:53:15.218 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.625,0.375) setter=(0.625,0.375) readback=(0.625,0.375) mode=normalized verified=1
2026-10-21 20:53:15.218 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.625,0.375) objectBounds=(-100,-50,200,100) canvas=(240,60) surface=320x240 origin=0
2026-10-21 20:53:15.268 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
2026-10-21 20:53:15.268 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(144,180) surface=320x240 origin=0
2026-10-21 20:53:15.318 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 20:53:15.318 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0,1) objectBounds=(-100,-50,200,100) canvas=(100,140) surface=320x240 origin=0
2026-10-21 20:53:15.369 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(nan,nan) surface=320x240 origin=0
2026-10-21 20:53:15.418 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 20:53:15.418 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(inf,nan) surface=320x240 origin=0
2026-10-21 20:53:15.467 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 20:53:15.467 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,nan) surface=320x240 origin=0
2026-10-21 20:53:15.517 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 20:53:15.517 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
OSC adapter: 98 checks, 0 failures
@@ -0,0 +1,5 @@
Beauty renderer: 49 checks, 0 failures (Metal).
2026-10-21 20:58:16.457 face-tests[11385:5472531] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
Face processing: 116 checks passed
2026-10-21 20:58:19.882 slim-tests[11391:5472584] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
PASS: 394 slim alignment checks; maximum UV oracle error 1.09828022e-05
仅有空白字符变更。
文件差异过大,未显示。 加载差异
@@ -0,0 +1,22 @@
PASS: independent remote recovery
Remote: https://dayu.media/git/dayu/paintfcp-fxplug.git
SHA: ad54726b17fabd11f46bc6121e664eac38960a23
Temporary clone: /tmp/paintfcp-recovery-ad54726.nARdL7/checkout
Temporary clone removed: True
Tracked files: 87
Git blob / checked-out bytes / modes mismatches: 0
Git fsck --strict --full exit: 0
LFS pointers: 0; submodules: 0
Resource files verified: 14; missing required: 0
Drop Zone.tiff: Templates/大鱼人像美化/Drop Zone.tiff: TIFF image data, big-endian, direntries=21, height=1200, bps=0, compression=LZW, PhotometricIntepretation=RGB, orientation=upper-left, width=1200
Core test exit: 0
Core counts: Beauty49 + Face116 + Slim394 =559; failures0
Maximum UV oracle error: 1.09828022e-05
Core test duration: 5.394s
ASan requested: false; sanitizer diagnostics: 0
Restored worktree after tests: clean
APP outputs:0; no xcodebuild build / host operations / root source edits / push
All modes exactly match Git; scripts/test.sh, test-host.sh, test-osc.sh and deliver.py restore executable.
External Apple SDK/frameworks are installed on this machine and must be installed separately when recovering.
Finished UTC: 2026-10-21T12:58:52.192540+00:00
Detailed JSON: /Users/dayu/Documents/PaintFCP/build/pose-analysis/recovery-v106/recovery-ad54726.json
@@ -0,0 +1,2 @@
2026-10-21 20:50:27.412 slim-chin-asan[10123:5460221] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
PASS: 494 slim alignment checks; maximum UV oracle error 1.09828022e-05
+5
查看文件
@@ -0,0 +1,5 @@
Beauty renderer: 49 checks, 0 failures (Metal).
2026-10-21 22:48:21.085 face-tests[18460:5636636] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
Face processing: 116 checks passed
2026-10-21 22:48:24.576 slim-tests[18472:5636747] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
PASS: 394 slim alignment checks; maximum UV oracle error 1.09828022e-05
@@ -0,0 +1,44 @@
{
"status": "installed_user_qa_pending",
"bundleIdentifier": "com.dayu.beatygril",
"installationPath": "/Library/Plug-Ins/FxPlug/beatygril.app",
"options": {
"waitForHosts": true,
"userTests": true
},
"nativeEffectQA": {
"status": "pending_user",
"performedByScript": false
},
"source": {
"commit": "f79d24eb0185f31feaae58b83b632f92dcb3ca91",
"trackedWorktreeChanges": []
},
"updatedAt": "2026-10-21T15:02:07.218005+00:00",
"version": "1.1.0",
"build": "9",
"stagingSignature": "Executable=/private/tmp/paintfcp-build-20261021/Build/Products/Release/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=3611af053e7fa8c0969e2bb6f600b7e9775b1f00\nCandidateCDHashFull sha256=3611af053e7fa8c0969e2bb6f600b7e9775b1f000a0142f2f50b833a1fc2b494\nHash choices=sha256\nCMSDigest=3611af053e7fa8c0969e2bb6f600b7e9775b1f000a0142f2f50b833a1fc2b494\nCMSDigestType=2\nCDHash=3611af053e7fa8c0969e2bb6f600b7e9775b1f00\nSignature size=8968\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 22:53:52\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n",
"notarization": {
"id": "ce26be63-9dcd-436a-bcb5-bd294fcc92a9",
"status": "Accepted",
"message": "Processing complete"
},
"signingVerified": true,
"stapleValidated": true,
"stagingPluginUnregistered": true,
"runningHosts": [],
"installedSignature": "Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=3611af053e7fa8c0969e2bb6f600b7e9775b1f00\nCandidateCDHashFull sha256=3611af053e7fa8c0969e2bb6f600b7e9775b1f000a0142f2f50b833a1fc2b494\nHash choices=sha256\nCMSDigest=3611af053e7fa8c0969e2bb6f600b7e9775b1f000a0142f2f50b833a1fc2b494\nCMSDigestType=2\nCDHash=3611af053e7fa8c0969e2bb6f600b7e9775b1f00\nSignature size=8968\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 22:53:52\nNotarization Ticket=stapled\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n",
"installedSignatureVerified": true,
"installedStapleValidated": true,
"gatekeeperAccepted": true,
"pluginDiscovery": {
"bundleIdentifier": "com.dayu.beatygril.fxplug",
"formalPath": "/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit",
"output": " com.dayu.beatygril.fxplug(1.1)\tDA452CD1-7B7F-4F15-9C52-09270028E946\t2026-10-21 15:02:07 +0000\t/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit\n (1 plug-in)\n"
},
"removedGeneratedPaths": [
"/Users/dayu/Documents/PaintFCP/build/delivery/previous-beatygril.app",
"/tmp/paintfcp-build-20261021",
"/Users/dayu/Documents/PaintFCP/build/delivery/notarization.zip"
]
}
+21
查看文件
@@ -0,0 +1,21 @@
2026-10-21 22:52:28.194 host-tests[18824:5644007] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
Default IOSurface maximum byte difference: 0
Alternate destination tag maximum byte difference: 0
Anatomy default 0/0 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
Anatomy default 0/1 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
Anatomy default 1/0 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
Anatomy default 1/1 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
Anatomy big-eye 0/0 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
Anatomy big-eye 0/1 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
Anatomy big-eye 1/0 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
Anatomy big-eye 1/1 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
Anatomy chin-positive 0/0 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
Anatomy chin-positive 0/1 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
Anatomy chin-positive 1/0 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
Anatomy chin-positive 1/1 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
Anatomy chin-negative 0/0 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
Anatomy chin-negative 0/1 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
Anatomy chin-negative 1/0 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
Anatomy chin-negative 1/1 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
Legacy fallback upright comparison: max=2 mean=0.0010357
Host adapter: 248 checks, 0 failures
二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 482 KiB

@@ -0,0 +1 @@
Inspector: 134 checks, 0 failures
@@ -0,0 +1,2 @@
com.dayu.beatygril.fxplug(1.1) DA452CD1-7B7F-4F15-9C52-09270028E946 2026-10-21 15:02:07 +0000 /Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit
(1 plug-in)
@@ -0,0 +1,24 @@
Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril
Identifier=com.dayu.beatygril
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha256=3611af053e7fa8c0969e2bb6f600b7e9775b1f00
CandidateCDHashFull sha256=3611af053e7fa8c0969e2bb6f600b7e9775b1f000a0142f2f50b833a1fc2b494
Hash choices=sha256
CMSDigest=3611af053e7fa8c0969e2bb6f600b7e9775b1f000a0142f2f50b833a1fc2b494
CMSDigestType=2
CDHash=3611af053e7fa8c0969e2bb6f600b7e9775b1f00
Signature size=8968
Authority=Developer ID Application: zhong luo (B3W25FX5H5)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Oct 21, 2026 at 22:53:52
Notarization Ticket=stapled
Info.plist entries=22
TeamIdentifier=B3W25FX5H5
Runtime Version=27.0.0
Sealed Resources version=2 rules=13 files=4
Internal requirements count=1 size=180
Total signatures=1
Chosen signature=1
@@ -0,0 +1,91 @@
{
"version": "1.1.0",
"build": "9",
"date": "2026-10-21T15:03:33.815750+00:00",
"sourceCommit": "f79d24eb0185f31feaae58b83b632f92dcb3ca91",
"uiOperations": false,
"hostTerminationPerformed": false,
"nativeVisualQA": "pending user",
"installationPath": "/Library/Plug-Ins/FxPlug/beatygril.app",
"codesign": {
"exitCode": 0,
"stdout": "",
"stderr": ""
},
"staple": {
"exitCode": 0,
"stdout": "Processing: /Library/Plug-Ins/FxPlug/beatygril.app\nThe validate action worked!",
"stderr": ""
},
"gatekeeper": {
"exitCode": 0,
"stdout": "",
"stderr": "/Library/Plug-Ins/FxPlug/beatygril.app: accepted\nsource=Notarized Developer ID"
},
"identity": [
{
"path": "/Library/Plug-Ins/FxPlug/beatygril.app",
"bundleIdentifier": "com.dayu.beatygril",
"version": "1.1.0",
"build": "9",
"archs": "x86_64 arm64",
"signature": "Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=3611af053e7fa8c0969e2bb6f600b7e9775b1f00\nCandidateCDHashFull sha256=3611af053e7fa8c0969e2bb6f600b7e9775b1f000a0142f2f50b833a1fc2b494\nHash choices=sha256\nCMSDigest=3611af053e7fa8c0969e2bb6f600b7e9775b1f000a0142f2f50b833a1fc2b494\nCMSDigestType=2\nCDHash=3611af053e7fa8c0969e2bb6f600b7e9775b1f00\nSignature size=8968\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 22:53:52\nNotarization Ticket=stapled\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1"
},
{
"path": "/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit",
"bundleIdentifier": "com.dayu.beatygril.fxplug",
"version": "1.1.0",
"build": "9",
"archs": "x86_64 arm64",
"signature": "Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit/Contents/MacOS/beatygril XPC Service\nIdentifier=com.dayu.beatygril.fxplug\nFormat=bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=837 flags=0x10000(runtime) hashes=19+3 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=c9b6ba40f307b1e1134890cae999a46b7c51750b\nCandidateCDHashFull sha256=c9b6ba40f307b1e1134890cae999a46b7c51750b599514a0e5858a1fc9618926\nHash choices=sha256\nCMSDigest=c9b6ba40f307b1e1134890cae999a46b7c51750b599514a0e5858a1fc9618926\nCMSDigestType=2\nCDHash=c9b6ba40f307b1e1134890cae999a46b7c51750b\nSignature size=8969\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 22:53:51\nInfo.plist entries=26\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=188\nTotal signatures=1\nChosen signature=1"
}
],
"template": {
"installedWithHostsClosed": true,
"files": [
{
"path": "/Users/dayu/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/大鱼人像美化.moef",
"bytes": 23073,
"sha256": "e1c0cff1dd3541ed97cab2cb39a76ad4f28dc659b3be7991cc7ac92dd8ea090b"
},
{
"path": "/Users/dayu/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/Drop Zone.tiff",
"bytes": 420128,
"sha256": "a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e"
}
],
"publicationCount": 3,
"manualAnalysisPublished": false
},
"discovery": "com.dayu.beatygril.fxplug(1.1)\tDA452CD1-7B7F-4F15-9C52-09270028E946\t2026-10-21 15:02:07 +0000\t/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit\n (1 plug-in)",
"duplicateInventory": {
"scopes": [
"/Users/dayu/Documents/PaintFCP",
"/Users/dayu/.codex/worktrees",
"/Users/dayu/Library/Developer/Xcode/DerivedData",
"/Users/dayu/Desktop",
"/Users/dayu/Downloads",
"/Applications",
"/Library/Plug-Ins/FxPlug",
"/Users/dayu/Library/Plug-Ins/FxPlug",
"/private/tmp/paintfcp-chin-smoke.rfco16hq",
"/private/tmp/paintfcp-format-probe.PsxvYQ",
"/private/tmp/paintfcp-live-review.ItH4EG",
"/private/tmp/paintfcp-rea.b3j9Te",
"/private/tmp/paintfcp-slim-field.6fDdJj",
"/private/tmp/paintfcp-slim-smoke.3ncdDe",
"/private/tmp/paintfcp-y-review.KpPi8T"
],
"matchingOuterApps": [
"/Library/Plug-Ins/FxPlug/beatygril.app"
],
"errors": [],
"scopeNote": "Bounded scan of project, worktrees, known build locations, Applications and FxPlug directories; not all mounted volumes."
},
"generatedCopyCleanup": {
"/Users/dayu/Documents/PaintFCP/build/delivery/previous-beatygril.app": true,
"/tmp/paintfcp-build-20261021": true,
"/Users/dayu/Documents/PaintFCP/build/delivery/notarization.zip": true
},
"status": "installed, formal identity/ticket/discovery/template verified; user native QA pending"
}
@@ -0,0 +1 @@
{"id":"ce26be63-9dcd-436a-bcb5-bd294fcc92a9","status":"Accepted","message":"Processing complete"}
+21
查看文件
@@ -0,0 +1,21 @@
2026-10-21 22:48:19.739 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
2026-10-21 22:48:19.745 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,180) surface=320x240 origin=0
2026-10-21 22:48:20.198 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.8,0.3) setter=(1152,-432) readback=(0.8,0.3) mode=centered-pixels verified=1
2026-10-21 22:48:20.198 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.7,0.4) objectBounds=(-1920,-1080,3840,2160) canvas=(152,78) surface=320x240 origin=0
2026-10-21 22:48:20.246 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.8,0.3) setter=(0.8,0.3) readback=(0.12345,0.87654) mode=normalized verified=0
2026-10-21 22:48:20.247 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.75,0.25) setter=(0.75,0.25) readback=(0.75,0.25) mode=normalized verified=1
2026-10-21 22:48:20.247 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.75,0.25) objectBounds=(-100,-50,200,100) canvas=(160,60) surface=320x240 origin=0
2026-10-21 22:48:20.298 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.625,0.375) setter=(0.625,0.375) readback=(0.625,0.375) mode=normalized verified=1
2026-10-21 22:48:20.298 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.625,0.375) objectBounds=(-100,-50,200,100) canvas=(240,60) surface=320x240 origin=0
2026-10-21 22:48:20.348 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
2026-10-21 22:48:20.349 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(144,180) surface=320x240 origin=0
2026-10-21 22:48:20.398 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 22:48:20.398 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0,1) objectBounds=(-100,-50,200,100) canvas=(100,140) surface=320x240 origin=0
2026-10-21 22:48:20.447 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(nan,nan) surface=320x240 origin=0
2026-10-21 22:48:20.500 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 22:48:20.500 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(inf,nan) surface=320x240 origin=0
2026-10-21 22:48:20.546 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 22:48:20.547 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,nan) surface=320x240 origin=0
2026-10-21 22:48:20.608 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 22:48:20.608 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
OSC adapter: 98 checks, 0 failures
@@ -0,0 +1 @@
Inspector: 134 checks, 0 failures
@@ -0,0 +1,75 @@
{
"status": "structure_verified_native_loading_pending",
"templateSHA256": "e1c0cff1dd3541ed97cab2cb39a76ad4f28dc659b3be7991cc7ac92dd8ea090b",
"pluginVersion": "1.1.0",
"publications": [
{
"object": "510000011",
"channel": "./2/100",
"name": "FCP 版本作者→"
},
{
"object": "510000012",
"channel": "./2/100",
"name": "更多模板浏览器访问→"
},
{
"object": "510000005",
"channel": "./210",
"name": ""
}
],
"parameterPathsResolved": 50,
"sourceMediaClosed": true,
"mediaSHA256": "a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e",
"customState": {
"class": "PGInspectorState",
"schema": 1,
"hasEdits": false,
"values": {
"41": 0.0,
"30": 1.0,
"71": 1.0,
"27": 0.0,
"5": 1.0,
"35": 0.0,
"24": 0.0,
"2": 0.0,
"32": 0.0,
"40": 1.7999999523162842,
"70": 1.0,
"18": 0.0,
"29": 0.0,
"26": 0.0,
"15": 0.0,
"4": 0.0,
"23": 0.0,
"12": 0.0,
"42": 0.0,
"72": 1.0,
"31": 0.0,
"1": 1.0,
"39": 0.0,
"28": 0.0,
"17": 0.0,
"25": 0.0,
"14": 0.0,
"3": 8.0,
"33": 0.0,
"22": 0.0
},
"disabled": [],
"classes": [
"NSDictionary",
"NSSet",
"PGInspectorState"
],
"innerBytes": 935,
"innerSHA256": "257540ac6079756a76f3d31dff1b9bca2b33393962057927791316965495ab60",
"outerBytes": 1127,
"outerSHA256": "95d5f1e2c35c40e9f1234b1480894bcc0ee68c0822c9bd3f7c04338062e6a26b",
"encodedCharacters": 1516,
"exactTextRoundtrip": true
},
"nativeLoading": "pending user; no host executed"
}
文件差异过大,未显示。 加载差异
@@ -0,0 +1,11 @@
Target: f79d24eb0185f31feaae58b83b632f92dcb3ca91
Remote: https://dayu.media/git/dayu/paintfcp-fxplug.git
Passed: True
Tracked files: 111
All tracked bytes/blob IDs/modes: True
Git fsck --strict --full: 0
LFS/submodules: [] / []
Inspector tests: {'checks': 134, 'failures': 0, 'ASan': True, 'exitCode': 0, 'log': 'inspector-tests-asan.txt'}
Template audit: structure_verified_native_loading_pending
Core 559 tests not repeated; no APP build/host/UI/sign/install.
Temporary clone removed: True
@@ -0,0 +1,75 @@
{
"status": "structure_verified_native_loading_pending",
"templateSHA256": "e1c0cff1dd3541ed97cab2cb39a76ad4f28dc659b3be7991cc7ac92dd8ea090b",
"pluginVersion": "1.1.0",
"publications": [
{
"object": "510000011",
"channel": "./2/100",
"name": "FCP 版本作者→"
},
{
"object": "510000012",
"channel": "./2/100",
"name": "更多模板浏览器访问→"
},
{
"object": "510000005",
"channel": "./210",
"name": ""
}
],
"parameterPathsResolved": 50,
"sourceMediaClosed": true,
"mediaSHA256": "a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e",
"customState": {
"class": "PGInspectorState",
"schema": 1,
"hasEdits": false,
"values": {
"41": 0.0,
"30": 1.0,
"71": 1.0,
"27": 0.0,
"5": 1.0,
"35": 0.0,
"24": 0.0,
"2": 0.0,
"32": 0.0,
"40": 1.7999999523162842,
"70": 1.0,
"18": 0.0,
"29": 0.0,
"26": 0.0,
"15": 0.0,
"4": 0.0,
"23": 0.0,
"12": 0.0,
"42": 0.0,
"72": 1.0,
"31": 0.0,
"1": 1.0,
"39": 0.0,
"28": 0.0,
"17": 0.0,
"25": 0.0,
"14": 0.0,
"3": 8.0,
"33": 0.0,
"22": 0.0
},
"disabled": [],
"classes": [
"NSDictionary",
"NSSet",
"PGInspectorState"
],
"innerBytes": 935,
"innerSHA256": "257540ac6079756a76f3d31dff1b9bca2b33393962057927791316965495ab60",
"outerBytes": 1127,
"outerSHA256": "95d5f1e2c35c40e9f1234b1480894bcc0ee68c0822c9bd3f7c04338062e6a26b",
"encodedCharacters": 1516,
"exactTextRoundtrip": true
},
"nativeLoading": "pending user; no host executed"
}
@@ -0,0 +1,33 @@
{
"scope": "offscreen AppKit inspector, real production controller and mocked host callbacks; no Motion/FCP UI",
"fullChecks": 1039,
"counts": {
"beauty": 49,
"face": 116,
"thinChin": 394,
"hostAdapterASan": 248,
"oscASan": 98,
"inspectorASan": 134
},
"nativeVisualQA": "pending user",
"manualAnalysisUI": "removed; legacy IDs hidden and buttons disabled",
"renderMode": "realtime automatic follow; stored manual geometry never used",
"templatePublishedEntries": 3,
"offscreenWidths": [
280,
320,
380,
520
],
"independentViewChecks": 27,
"fixedRegressions": [
"shape group bypass clears face-box overlay",
"disabled legacy detail effects stay disabled on first edit",
"legacy global smoothing remains enabled when its old local group was off",
"new gesture reads current host after immediate undo",
"pending synchronization is bound to CMTime",
"same-time host change refreshes without waiting",
"explicit numeric input is never skipped due to stale UI cache"
],
"archives": "Own PGInspectorState secure payload; Foundation outer and inner decode and Motion alphabet roundtrip tested"
}
+61 -1
查看文件
@@ -22,7 +22,7 @@ Motion:独立普通视频项目,源PTS0、1920×1080/25fps/10秒,媒体通
用户给出的MS超级美颜V2.moef共发布15个入口,没有跟踪、前向或后向分析控件。没有额外分析按钮不影响原版默认自动跟随;活跃render路径与Motion运行均已确认这一点。复刻版的三目标离线分析与持久化属于独立扩展,应与默认实时跟随分开理解。 用户给出的MS超级美颜V2.moef共发布15个入口,没有跟踪、前向或后向分析控件。没有额外分析按钮不影响原版默认自动跟随;活跃render路径与Motion运行均已确认这一点。复刻版的三目标离线分析与持久化属于独立扩展,应与默认实时跟随分开理解。
六种肤色风格采用自写调色变换,脸型使用基于Vision关键点的近似局部反向采样;未复制原版私有LUT或算法资源。没有完成原版逐像素对齐、全部脸型强度主观比对、多人交叉遮挡、所有宿主版本及Intel硬件验收。源码层保存/恢复测试不代替两宿主所有工程关闭重开场景。 六种肤色风格采用自写调色变换,脸型当前使用Vision脸框场与眼部点位置的近似反向采样;瘦脸本身没有使用下颌轮廓点。未复制原版私有LUT或算法资源。没有完成原版逐像素对齐、全部脸型强度主观比对、多人交叉遮挡、所有宿主版本及Intel硬件验收。源码层保存/恢复测试不代替两宿主所有工程关闭重开场景。
## 自动化与恢复 ## 自动化与恢复
@@ -41,3 +41,63 @@ Motion:独立普通视频项目,源PTS0、1920×1080/25fps/10秒,媒体通
原版完整数学系数未恢复,本版使用自有平滑公式与past-only时间策略;当前检测框未做原版77点网格级稳定,没有真正sourceID,不能声称任意同尺寸换源或首次乱序完全一致。原生1.0.4:Motion成功加载正式路径插件,实际视频在不同播放头位置正常显示提亮、脸型和黄色主要人脸框;项目未执行离线分析,“使用跟踪数据”读取为0。用户随后明确接管Motion/FCP进行自测,自动化已停止,因此尚未完成1.0.4的FCP自动跟随、显式OSC点击回写及两宿主复杂场景验收。FCP黑屏输入链与提亮此前已原生复测,不能把它们混称为1.0.4所有自动跟随场景通过。详见分析说明的具体边界。 原版完整数学系数未恢复,本版使用自有平滑公式与past-only时间策略;当前检测框未做原版77点网格级稳定,没有真正sourceID,不能声称任意同尺寸换源或首次乱序完全一致。原生1.0.4:Motion成功加载正式路径插件,实际视频在不同播放头位置正常显示提亮、脸型和黄色主要人脸框;项目未执行离线分析,“使用跟踪数据”读取为0。用户随后明确接管Motion/FCP进行自测,自动化已停止,因此尚未完成1.0.4的FCP自动跟随、显式OSC点击回写及两宿主复杂场景验收。FCP黑屏输入链与提亮此前已原生复测,不能把它们混称为1.0.4所有自动跟随场景通过。详见分析说明的具体边界。
代码节点已从远端独立克隆恢复,51项受跟踪文件与9项必要资源逐字节/对象/文件模式一致;380项检查、严格fsck与Xcode工程读取全部通过,无LFS或子模块。验收报告见 [独立恢复摘要](verification/recovery-e03e460.txt) 与 [完整记录](verification/recovery-e03e460.json)。最终文档标签另作独立克隆核验,源码构建依赖没有因文档更新改变。 代码节点已从远端独立克隆恢复,51项受跟踪文件与9项必要资源逐字节/对象/文件模式一致;380项检查、严格fsck与Xcode工程读取全部通过,无LFS或子模块。验收报告见 [独立恢复摘要](verification/recovery-e03e460.txt) 与 [完整记录](verification/recovery-e03e460.json)。最终文档标签另作独立克隆核验,源码构建依赖没有因文档更新改变。
## 用户复测与瘦脸专项分析
用户反馈“跟踪没问题”,同时提供最大瘦脸与侧脸变形对照。记录为用户实测反馈,不扩张为显式OSC点击、两宿主复杂场景或全部脸型验收通过。
后续静态REA和Apple Metal AIR分析确认:原版瘦脸以17组轮廓到鼻部/瞳孔锚点的局部位移驱动,当前瘦脸按框水平对称变形,两者的方向、范围与强度模型不同。独立Vision76点离线探针在同一视频11个采样时刻均检出1脸76点,含明显侧脸;它只证明关键点仍可获得,不代替原版宿主输出对照。探针使用Wall/Wextra编译成功并完成采样,没有执行原版APP、替换安装版或操作Motion/FCP。
分析报告见 [脸型变形与转脸分析](analysis/脸型变形与转脸分析.md)。当前安装1.0.4的变形问题尚未修复,本轮的诊断代码/证据/文档不改变已安装产品。
## 1.0.5 瘦脸修复
已将最大瘦脸改为76点、17组局部位移;明确revision3/76点,保留图像及四类蒙版共用变形,旧档案缺完整点阵时只跳过瘦脸。新增224项数值与数据回归:48组尺寸/左右侧脸/歪头投影、非零原点、0/50/100%强度、独立double逆向坐标场、旧档案/坏点保护、失检重获及真实Vision76点与存储回放。最大UV坐标误差1.09828022e-5。
完整回归604项通过(渲染49、人脸116、瘦脸224、宿主适配117、OSC98);宿主适配与OSC启用ASan。独立审查另用程序生成的UV图与1×1读取验证36点,最大误差1.12695e-7。位图数值测试曾发现测试输入行方向与CI坐标不一致,修正测试桥接后通过,生产算法未因此反改。
1.0.5(7)已完成通用Release编译、Developer ID逐层签名、Apple公证Accepted(7d7034f8-7389-410f-ad8e-9e722408c0fc)、票据粘附/验证、Gatekeeper、安装身份与正式路径注册验证。用户保存退出宿主后安装;本轮没有操作或启动Motion/FCP界面,最终视觉由用户自测。默认单主要人脸对齐17-pin数学;快速多人仍沿用逐脸采样,重叠区不宣称与原版单fragment全脸序列等价。
额外用用户视频11帧(含4秒及18秒明显侧脸)运行264项瘦脸ASan检查,通过且未发现内存错误;此数包含重复的220项基础数值/数据检查,不与604简单相加。独立恢复从远程abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964克隆,69文件/9资源和模式一致、strict fsck通过,clone内389项Metal/人脸/瘦脸测试通过。签名构建源码就是该节点;后续交付脚本补充了已公证产物校验恢复与重复注销兼容,产品源码未再变化。
正式安装APP仅位于 `/Library/Plug-Ins/FxPlug/beatygril.app`;配套1.0.5模板及占位资源已同步并逐字节核验。临时构建、旧版APP备份、公证ZIP已清理;项目、DerivedData、桌面、下载、Applications及系统/用户FxPlug目录扫描只发现该正式APP,无扫描错误。原生效果验收状态为用户自测待完成。票据、发现、恢复与检测报告见 `docs/verification/v1.0.5/`。
## 1.0.6 下巴与图像轴修复(构建前)
用户反馈“下巴调整作用到眼睛”。纯CI76点物理位置并未全局反置,但FxPlug适配器曾将TOP_LEFT当作物理行序,在输入和输出各多翻转一次;默认直通会抵消,无法验证中间Vision和变形的解剖方向。独立top-first照片构造复现下巴跑向眼部;原版REA输入及两条CI→IOSurface输出路径均没有同样额外翻转,现删除这两处。未捕获本次实际FCP缓冲,原生视觉由用户自测。
下巴另有旧框高斯算法和正负符号错误,现按原版68→38、67→37、66→36三pin、正0.04/负0.02和共同尺寸增益对齐。数学场、真实图物理下巴/瞳孔、正负符号与侧脸热点均已检查;其余七类控制及全部组合顺序未宣称完整恢复。新分析记录写入显式坐标标记;旧未标记记录保留但不再重放,自动回退实时跟随,重新分析可恢复离线记录。
本轮不操作、启动或退出Motion/FCP。完整分析证据见[下巴与坐标分析](analysis/下巴与坐标分析.md)。完整904项通过(美化49、人脸116、瘦脸/下巴394、宿主适配247、OSC98),宿主适配和OSC启用ASan。宿主适配16种默认/大眼/下巴正负×源/目的原点组合彼此逐字节一致,与独立upright文件参考最大差2/255;独立文件解码和IOSurface检测路径存在微小数值差,阈值另限制均差<0.005/255,并检查实际解剖热点。额外用户视频11帧494项ASan通过,含重复基础用例,不与904简单相加。用户帧和完整点位未上传远程。当前正式APP仍为1.0.5,不能将构建前验证描述为1.0.6已安装。
1.0.6(8)通用Release的签名构建源码为`ad54726b17fabd11f46bc6121e664eac38960a23`,构建开始时工作区干净。Developer ID逐层签名/严格验证、Apple公证Accepted(ab0271c8-0c4b-4b10-9474-cebe9b55385b)、票据粘附/验证均已通过。检测到Motion仍运行,当前在产品交付锁内只读等待用户保存退出,尚未替换正式1.0.5。
签名源码已从大鱼远程独立克隆恢复:87个受跟踪文件的字节、blob和模式全部一致,14项资源完整,strict/full fsck通过,无LFS/子模块;clone内559项核心测试通过,未构建APP或操作宿主,临时克隆已清理。报告在`verification/v1.0.6/recovery-ad54726.json`。
## 1.0.6 正式安装完成
用户确认保存并退出Motion/FCP后,安装流程完成1.0.6(8)替换,正式APP为`/Library/Plug-Ins/FxPlug/beatygril.app`。安装版APP与嵌套插件均为x86_64/arm64,Developer ID团队B3W25FX5H5、可信时间戳和Hardened Runtime正确;codesign严格验证、票据验证、Gatekeeper与PluginKit正式路径发现通过。全过程没有操作、启动或退出宿主界面,原生视觉仍由用户自测。
配套1.0.6效果模板及Drop Zone.tiff已在宿主退出时同步并逐字节核验;moef SHA-256为`30ca8edf36801264c5de2c753c40b720aba8052633620713a12401098fcd2409`。模板仍保留固定UUID及原有参数发布。旧未标记的跟踪数据会回退实时跟随;需要离线重放时请重新分析目标。
临时Release APP、旧APP备份和公证ZIP已清理。项目、Codex工作树、DerivedData、桌面、下载、Applications、系统/用户FxPlug目录及本次paintfcp临时目录扫描仅发现正式路径外层APP,扫描错误0;没有将这项有界扫描表述为所有卷的完整搜索。完整回执、签名、发现、模板摘要和扫描范围在`verification/v1.0.6/`。最终发布标签为`v1.0.6`,产品源码与签名构建节点ad54726一致;其后的提交仅更新交付文档、模板说明和验证回执。
## 1.1.0 原生卡片面板(构建前)
按参考界面的深色圆角卡片、青/绿/蓝紫分组、自绘滑块与开关、肤色圆点和单项重置实现独立AppKit面板,使用大鱼自己的品牌文字。新增自定义参数210及NSSecureCoding状态对象;FCP发布由48入口缩为面板+版权两项共3入口,旧49项参数和组路径保留隐藏兼容,手动分析命令隐藏且禁用。实时渲染不再载入任何保存的手动分析点阵,也不带旧分析大数据进入render snapshot。
完整1039项通过(美化49、人脸116、瘦脸/下巴394、宿主适配248、OSC98、检查器134),后三类适配/检查器使用ASan。检查器覆盖单项路由、完整安全归档、Motion自定义包内实际inner/outer的Foundation解码、初始不覆盖旧文档、数值插值、启停保值、重置、失效写入、事务配对、快速撤销/切时间后新编辑、显式数值输入与窄宽度布局。独立审查另用mock验证27项视图交互;280/320/380/520点宽度及更多展开不越界。测试修复了分组关闭后框仍显示、旧细节禁用状态丢失、快速撤销/换帧旧值回灌和旧显示缓存拦截明确输入等问题。
参考原版仅用于静态结构和外观分析,自有归档不含原版类/私有数据/品牌图。面板PNG为离屏演示参数预览,不是FCP原生截图。尚未操作Motion/FCP界面,真实宿主嵌入滚动、键盘/VoiceOver及工程重开由用户自测。源码与回执会按正式交付节点保存。
1.1.0(9)的签名构建源码为`f79d24eb0185f31feaae58b83b632f92dcb3ca91`,干净工作区构建。通用Release的Developer ID签名、严格验证、Apple公证Accepted(ce26be63-9dcd-436a-bcb5-bd294fcc92a9)和票据验证已通过。安装前曾检测到Motion/FCP仍运行,交付流程持锁等待用户自行保存退出;退出后完成下述替换。没有操作宿主界面。
独立远程克隆恢复已核对111个文件的字节、blob和模式、16项必要资源、strict/full fsck,无LFS/子模块;clone内134项Inspector ASan及模板审计通过,临时克隆已清理,详见`verification/v1.1.0/recovery-f79d24e.json`。
## 1.1.0 正式安装完成
检测时Motion/FCP已退出,交付流程完成1.1.0(9)替换。实际安装版及嵌套插件均为arm64/x86_64,签名团队B3W25FX5H5、可信时间戳、Hardened Runtime和固定Bundle ID正确;严格签名、Apple公证Accepted、粘附票据、Gatekeeper及PluginKit唯一正式路径通过。没有发送强退信号或操作宿主界面。
配套卡片版模板和Drop Zone.tiff已在宿主关闭时同步并逐字节核对,moef SHA256为`e1c0cff1dd3541ed97cab2cb39a76ad4f28dc659b3be7991cc7ac92dd8ea090b`。FCP发布入口为面板210及两项版权信息;旧参数定义隐藏保留,手动分析不再显示,输出保持实时自动跟随。自定义面板的真实嵌入、滚动、键盘/VoiceOver、保存重开及视觉均待用户自测。
临时APP、旧APP备份、公证ZIP已清理。项目、Codex工作树、DerivedData、桌面、下载、Applications、系统/用户FxPlug及paintfcp临时目录的有界扫描仅发现正式外层APP,扫描错误0;不将该范围扩张为全部挂载卷搜索。完整回执和扫描范围见`verification/v1.1.0/installed-verification.json`。最终恢复标签`v1.1.0`包含交付文档,签名产品源码保持f79d24eb0185f31feaae58b83b632f92dcb3ca91。
+117 -13
查看文件
@@ -2,8 +2,13 @@
"""Release, notarize and install the unique FxPlug app under its delivery lock. """Release, notarize and install the unique FxPlug app under its delivery lock.
The final prompt keeps the lock held while native-host validation is performed. The final prompt keeps the lock held while native-host validation is performed.
""" """
import json, os, pathlib, plistlib, shutil, subprocess, sys, time import argparse, datetime, json, os, pathlib, plistlib, re, shutil, subprocess, sys, time
root=pathlib.Path(__file__).resolve().parent.parent root=pathlib.Path(__file__).resolve().parent.parent
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--wait-for-hosts',action='store_true',help='After notarization, retain the delivery lock and wait for Motion/FCP to exit naturally before installing; never control or quit either host.')
parser.add_argument('--user-tests',action='store_true',help='After non-UI installed-app verification, record native effect QA as pending user testing and clean known generated copies without the interactive validation prompt.')
parser.add_argument('--resume-prepared',action='store_true',help='Resume an already Accepted and stapled staged app after checking its recorded signature, version and unchanged product source.')
args=parser.parse_args()
bundle_id='com.dayu.beatygril' bundle_id='com.dayu.beatygril'
lock=pathlib.Path('/Users/dayu/.codex/app-delivery-locks')/(bundle_id+'.lock') lock=pathlib.Path('/Users/dayu/.codex/app-delivery-locks')/(bundle_id+'.lock')
lock.parent.mkdir(parents=True,exist_ok=True) lock.parent.mkdir(parents=True,exist_ok=True)
@@ -18,44 +23,129 @@ archive=logs/'notarization.zip'
identity='Developer ID Application: zhong luo (B3W25FX5H5)' identity='Developer ID Application: zhong luo (B3W25FX5H5)'
keychain='/Users/dayu/Library/Keychains/login.keychain-db' keychain='/Users/dayu/Library/Keychains/login.keychain-db'
previous=logs/'previous-beatygril.app' previous=logs/'previous-beatygril.app'
receipt_path=logs/'delivery-receipt.json'
receipt={'status':'preparing','bundleIdentifier':bundle_id,'installationPath':str(installed),
'options':{'waitForHosts':args.wait_for_hosts,'userTests':args.user_tests},
'nativeEffectQA':{'status':'pending_user' if args.user_tests else 'pending_operator','performedByScript':False}}
def run(args, log=None): def run(args, log=None):
if log: if log:
with open(log,'w') as output:subprocess.run(args,cwd=root,stdout=output,stderr=subprocess.STDOUT,check=True) with open(log,'w') as output:subprocess.run(args,cwd=root,stdout=output,stderr=subprocess.STDOUT,check=True)
return '' return ''
return subprocess.run(args,cwd=root,text=True,capture_output=True,check=True).stdout return subprocess.run(args,cwd=root,text=True,capture_output=True,check=True).stdout
def save_receipt(status,**fields):
receipt.update(fields);receipt['status']=status
receipt['updatedAt']=datetime.datetime.now(datetime.timezone.utc).isoformat()
receipt_path.write_text(json.dumps(receipt,ensure_ascii=False,indent=2)+'\n')
def running_hosts():
# Read process state only. Match executable suffixes so renamed installation
# roots do not accidentally allow replacing a live host's plug-in connection.
suffixes=('/Motion.app/Contents/MacOS/Motion','/Final Cut Pro.app/Contents/MacOS/Final Cut Pro')
found=[]
for line in run(['ps','-axo','pid=,comm=']).splitlines():
columns=line.strip().split(None,1)
if len(columns)==2 and columns[1].endswith(suffixes):found.append({'pid':int(columns[0]),'executable':columns[1]})
return found
def await_host_exit():
active=running_hosts()
if active and not args.wait_for_hosts:
raise RuntimeError('Close Motion and Final Cut Pro before installing; installed app retained')
if active:
save_receipt('waiting_for_host_exit',runningHosts=active)
print('HOST_EXIT_REQUIRED: Motion/Final Cut Pro must exit naturally before installation. The delivery lock remains held; no host/UI action will be taken.',flush=True)
while active:
time.sleep(3)
active=running_hosts()
print('HOSTS_EXITED: continuing with installation under the same delivery lock.',flush=True)
save_receipt('ready_to_install',runningHosts=[])
def require_product(path):
info=plistlib.loads((path/'Contents/Info.plist').read_bytes())
if info.get('CFBundleIdentifier')!=bundle_id:raise RuntimeError('Generated/installed app identity mismatch: '+str(path))
return info
def unregister_generated_plugin(plugin):
# Removing an already absent registration can return 1. Establish absence
# by discovery rather than turning that idempotent state into a failure.
removed=subprocess.run(['pluginkit','-r',str(plugin)],capture_output=True,text=True)
info=plistlib.loads((plugin/'Contents/Info.plist').read_bytes())
discovered=run(['pluginkit','-m','-A','-D','-v','-i',info['CFBundleIdentifier']])
if str(plugin) in discovered:raise RuntimeError('Generated plug-in is still discoverable: '+removed.stderr)
def cleanup_known_products():
removed=[]
if previous.exists():
require_product(previous);shutil.rmtree(previous);removed.append(str(previous))
if staging.exists():
if app.exists():require_product(app)
shutil.rmtree(staging);removed.append(str(staging))
if archive.exists():archive.unlink();removed.append(str(archive))
return removed
try: try:
delivery_source={'commit':run(['git','rev-parse','HEAD']).strip(),
'trackedWorktreeChanges':run(['git','status','--porcelain','--untracked-files=no']).splitlines()}
plugin=app/'Contents/PlugIns/beatygril XPC Service.pluginkit'
if args.resume_prepared:
old_receipt=json.loads(receipt_path.read_text())
result=old_receipt.get('notarization',{})
if result.get('status')!='Accepted':raise RuntimeError('No Accepted staged notarization to resume')
info=require_product(app)
run(['codesign','--verify','--deep','--strict',str(app)])
run(['xcrun','stapler','validate',str(app)])
signature=subprocess.run(['codesign','-dvvv',str(app)],capture_output=True,text=True,check=True).stderr
expected=re.search(r'^CDHash=(.+)$',old_receipt.get('stagingSignature',''),re.M)
actual=re.search(r'^CDHash=(.+)$',signature,re.M)
if not expected or not actual or expected.group(1)!=actual.group(1):raise RuntimeError('Prepared signature hash changed')
if info.get('CFBundleShortVersionString')!=old_receipt.get('version') or info.get('CFBundleVersion')!=old_receipt.get('build'):raise RuntimeError('Prepared version changed')
source_commit=old_receipt.get('source',{}).get('commit')
if not source_commit or run(['git','diff',source_commit,'--','beatygril']).strip():raise RuntimeError('Product source changed since prepared build')
receipt.update(old_receipt);receipt.pop('failure',None)
receipt['resumedBy']=delivery_source
receipt['options']={'waitForHosts':args.wait_for_hosts,'userTests':args.user_tests,'resumePrepared':True}
receipt['nativeEffectQA']={'status':'pending_user' if args.user_tests else 'pending_operator','performedByScript':False}
save_receipt('resuming_prepared')
print('Previously Accepted and stapled Release verified; resuming installation.',flush=True)
else:
receipt['source']=delivery_source
save_receipt('building')
identities=run(['security','find-identity','-v','-p','codesigning',keychain]) identities=run(['security','find-identity','-v','-p','codesigning',keychain])
if identity not in identities:raise RuntimeError('Valid Developer ID Application identity is unavailable') if identity not in identities:raise RuntimeError('Valid Developer ID Application identity is unavailable')
print('Building universal Release…',flush=True) print('Building universal Release…',flush=True)
run(['xcodebuild','-project','beatygril/beatygril.xcodeproj','-scheme','Wrapper Application','-configuration','Release','-derivedDataPath',str(staging),'-destination','generic/platform=macOS','build'],logs/'build.log') run(['xcodebuild','-project','beatygril/beatygril.xcodeproj','-scheme','Wrapper Application','-configuration','Release','-derivedDataPath',str(staging),'-destination','generic/platform=macOS','build'],logs/'build.log')
plugin=app/'Contents/PlugIns/beatygril XPC Service.pluginkit' # Xcode can register a fresh build. Remove this known generated path before
# lengthy signing/notarization while a user may still be testing the old app.
unregister_generated_plugin(plugin)
for framework in sorted((plugin/'Contents/Frameworks').glob('*.framework')): for framework in sorted((plugin/'Contents/Frameworks').glob('*.framework')):
run(['codesign','--force','--timestamp','--options','runtime','--sign',identity,str(framework)]) run(['codesign','--force','--timestamp','--options','runtime','--sign',identity,str(framework)])
run(['codesign','--force','--timestamp','--options','runtime','--sign',identity,str(plugin)]) run(['codesign','--force','--timestamp','--options','runtime','--sign',identity,str(plugin)])
run(['codesign','--force','--timestamp','--options','runtime','--entitlements',str(root/'beatygril/beatygril/Wrapper Application/SandboxEntitlements.entitlements'),'--sign',identity,str(app)]) run(['codesign','--force','--timestamp','--options','runtime','--entitlements',str(root/'beatygril/beatygril/Wrapper Application/SandboxEntitlements.entitlements'),'--sign',identity,str(app)])
run(['codesign','--verify','--deep','--strict',str(app)]) run(['codesign','--verify','--deep','--strict',str(app)])
info=require_product(app)
receipt['version']=info.get('CFBundleShortVersionString');receipt['build']=info.get('CFBundleVersion')
receipt['stagingSignature']=subprocess.run(['codesign','-dvvv',str(app)],capture_output=True,text=True,check=True).stderr
run(['ditto','-c','-k','--keepParent',str(app),str(archive)]) run(['ditto','-c','-k','--keepParent',str(app),str(archive)])
print('Submitting signed app to Apple notarization…',flush=True) print('Submitting signed app to Apple notarization…',flush=True)
response=run(['xcrun','notarytool','submit',str(archive),'--keychain-profile','codex-dayu-notary-B3W25FX5H5','--keychain',keychain,'--wait','--output-format','json']) response=run(['xcrun','notarytool','submit',str(archive),'--keychain-profile','codex-dayu-notary-B3W25FX5H5','--keychain',keychain,'--wait','--output-format','json'])
(logs/'notarization.json').write_text(response) (logs/'notarization.json').write_text(response)
result=json.loads(response) result=json.loads(response)
receipt['notarization']=result
if result.get('status')!='Accepted': if result.get('status')!='Accepted':
if result.get('id'): if result.get('id'):
(logs/'notarization-details.json').write_text(run(['xcrun','notarytool','log',result['id'],'--keychain-profile','codex-dayu-notary-B3W25FX5H5','--keychain',keychain])) (logs/'notarization-details.json').write_text(run(['xcrun','notarytool','log',result['id'],'--keychain-profile','codex-dayu-notary-B3W25FX5H5','--keychain',keychain]))
raise RuntimeError('Apple did not accept notarization; original installed app retained') raise RuntimeError('Apple did not accept notarization; original installed app retained')
run(['xcrun','stapler','staple',str(app)]) run(['xcrun','stapler','staple',str(app)])
run(['xcrun','stapler','validate',str(app)]) run(['xcrun','stapler','validate',str(app)])
# Keep a prepared build out of host discovery while the user still has the
# old installed version open. This removes only this known staging path.
unregister_generated_plugin(plugin)
save_receipt('prepared',signingVerified=True,stapleValidated=True,stagingPluginUnregistered=True)
# A live host retains its old XPC/OSC connection. Replacing the bundle while # A live host retains its old XPC/OSC connection. Replacing the bundle while
# that connection exists can deadlock host shutdown or select stale code. # that connection exists can deadlock host shutdown or select stale code.
running=run(['ps','-axo','comm=']).splitlines() await_host_exit()
hosts={'/Applications/Motion.app/Contents/MacOS/Motion',
'/Applications/Final Cut Pro.app/Contents/MacOS/Final Cut Pro'}
if hosts.intersection(line.strip() for line in running):
raise RuntimeError('Close Motion and Final Cut Pro before installing; installed app retained')
if installed.exists(): if installed.exists():
info=plistlib.loads((installed/'Contents/Info.plist').read_bytes()) require_product(installed)
if info.get('CFBundleIdentifier')!=bundle_id:raise RuntimeError('The installation path belongs to another product')
if previous.exists():raise RuntimeError('Previous recovery app already exists') if previous.exists():raise RuntimeError('Previous recovery app already exists')
# Recheck immediately before touching the installed bundle. Waiting mode does
# not grant permission for a hot replacement if a host was opened again.
await_host_exit()
save_receipt('installing')
if installed.exists():
shutil.move(str(installed),str(previous)) shutil.move(str(installed),str(previous))
try: try:
shutil.copytree(app,installed,symlinks=True) shutil.copytree(app,installed,symlinks=True)
@@ -69,17 +159,31 @@ try:
run(['pluginkit','-a',str(installed/'Contents/PlugIns/beatygril XPC Service.pluginkit')]) run(['pluginkit','-a',str(installed/'Contents/PlugIns/beatygril XPC Service.pluginkit')])
# Xcode may register its build product. Only the installed plugin should # Xcode may register its build product. Only the installed plugin should
# be discoverable while native host validation is in progress. # be discoverable while native host validation is in progress.
run(['pluginkit','-r',str(plugin)]) unregister_generated_plugin(plugin)
installed_plugin=installed/'Contents/PlugIns/beatygril XPC Service.pluginkit'
plugin_info=plistlib.loads((installed_plugin/'Contents/Info.plist').read_bytes())
discovered=run(['pluginkit','-m','-A','-D','-v','-i',plugin_info['CFBundleIdentifier']])
if str(installed_plugin) not in discovered:raise RuntimeError('PluginKit did not report the formal installed plug-in path')
(logs/'installed-discovery.txt').write_text(discovered)
save_receipt('installed',installedSignature=signature,installedSignatureVerified=True,
installedStapleValidated=True,gatekeeperAccepted=True,
pluginDiscovery={'bundleIdentifier':plugin_info['CFBundleIdentifier'],'formalPath':str(installed_plugin),'output':discovered})
except Exception: except Exception:
if installed.exists():shutil.rmtree(installed) if installed.exists():shutil.rmtree(installed)
if previous.exists():shutil.move(str(previous),str(installed)) if previous.exists():shutil.move(str(previous),str(installed))
raise raise
print(json.dumps({'status':'installed','path':str(installed),'notarization':result}),flush=True) print(json.dumps({'status':'installed','path':str(installed),'notarization':result}),flush=True)
if args.user_tests:
print('USER_TESTS_PENDING: installed identity, ticket, Gatekeeper and formal-path discovery verified. Native effect QA is left to the user; no host/UI operation was performed.',flush=True)
else:
input('HOST_VALIDATION_PENDING: press Return only after host verification and duplicate inventory.\n') input('HOST_VALIDATION_PENDING: press Return only after host verification and duplicate inventory.\n')
receipt['nativeEffectQA']={'status':'operator_confirmed','performedByScript':False}
# Only this task’s known generated product copies are removed. # Only this task’s known generated product copies are removed.
if previous.exists():shutil.rmtree(previous) removed=cleanup_known_products()
if archive.exists():archive.unlink() save_receipt('installed_user_qa_pending' if args.user_tests else 'complete',removedGeneratedPaths=removed)
if staging.exists():shutil.rmtree(staging)
print('Delivery staging and notarization archive cleaned.',flush=True) print('Delivery staging and notarization archive cleaned.',flush=True)
except BaseException as error:
save_receipt('failed',failure={'type':type(error).__name__,'message':str(error)})
raise
finally: finally:
(lock/'owner.json').unlink();lock.rmdir() (lock/'owner.json').unlink();lock.rmdir()
+109
查看文件
@@ -0,0 +1,109 @@
// Offline diagnostic only. It uses Apple's public Vision API, never a host UI
// or the reference plug-in. User media and generated observations stay local.
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <Vision/Vision.h>
#import <ImageIO/ImageIO.h>
#include <math.h>
static NSArray *PGProbePoint(CGPoint p) { return @[@(p.x), @(p.y)]; }
static NSArray *RegionIndices(VNFaceLandmarkRegion2D *region, VNFaceLandmarkRegion2D *all) {
NSMutableArray *indices = [NSMutableArray array];
for (NSUInteger i=0; i<region.pointCount; i++) {
CGPoint p=region.normalizedPoints[i];
double best=INFINITY; NSUInteger index=NSNotFound;
for (NSUInteger j=0; j<all.pointCount; j++) {
CGPoint q=all.normalizedPoints[j];
double d=hypot(p.x-q.x,p.y-q.y);
if (d<best) { best=d; index=j; }
}
// Preserve an unmatched result instead of inventing a point identity.
[indices addObject:best<1e-6 ? @(index) : NSNull.null];
}
return indices;
}
int main(int argc, const char *argv[]) { @autoreleasepool {
if (argc<4) {
fprintf(stderr,"usage: inspect-face-geometry <video> <output.json> <seconds>...\n");
return 2;
}
NSURL *source=[NSURL fileURLWithPath:@(argv[1])];
NSURL *output=[NSURL fileURLWithPath:@(argv[2])];
if (![NSFileManager.defaultManager fileExistsAtPath:source.path]) return 2;
NSError *error=nil;
if (![NSFileManager.defaultManager createDirectoryAtURL:[output URLByDeletingLastPathComponent]
withIntermediateDirectories:YES attributes:nil error:&error]) {
fprintf(stderr,"%s\n",error.description.UTF8String); return 1;
}
AVURLAsset *asset=[AVURLAsset URLAssetWithURL:source options:nil];
AVAssetImageGenerator *generator=[[AVAssetImageGenerator alloc] initWithAsset:asset];
generator.appliesPreferredTrackTransform=YES;
generator.maximumSize=CGSizeMake(1280,1280);
generator.requestedTimeToleranceBefore=kCMTimeZero;
generator.requestedTimeToleranceAfter=kCMTimeZero;
NSMutableArray *frames=[NSMutableArray array];
for (int a=3; a<argc; a++) { @autoreleasepool {
char *end=NULL; double seconds=strtod(argv[a],&end);
if (!end || end==argv[a] || *end || !isfinite(seconds) || seconds<0) return 2;
CMTime actual=kCMTimeInvalid;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
CGImageRef image=[generator copyCGImageAtTime:CMTimeMakeWithSeconds(seconds,60000)
actualTime:&actual error:&error];
#pragma clang diagnostic pop
if (!image) { fprintf(stderr,"%s\n",error.description.UTF8String); return 1; }
VNDetectFaceLandmarksRequest *request=[VNDetectFaceLandmarksRequest new];
if (![VNDetectFaceLandmarksRequest.supportedRevisions containsIndex:VNDetectFaceLandmarksRequestRevision3] ||
![VNDetectFaceLandmarksRequest revision:VNDetectFaceLandmarksRequestRevision3
supportsConstellation:VNRequestFaceLandmarksConstellation76Points]) {
CGImageRelease(image); fprintf(stderr,"Vision revision 3 / 76 points unavailable\n"); return 1;
}
request.revision=VNDetectFaceLandmarksRequestRevision3;
request.constellation=VNRequestFaceLandmarksConstellation76Points;
VNImageRequestHandler *handler=[[VNImageRequestHandler alloc] initWithCGImage:image options:@{}];
if (![handler performRequests:@[request] error:&error]) {
CGImageRelease(image); fprintf(stderr,"%s\n",error.description.UTF8String); return 1;
}
NSMutableArray *faces=[NSMutableArray array];
for (VNFaceObservation *face in request.results) {
VNFaceLandmarks2D *lm=face.landmarks;
VNFaceLandmarkRegion2D *all=lm.allPoints;
CGRect box=face.boundingBox;
NSMutableArray *points=[NSMutableArray array];
for (NSUInteger i=0; i<all.pointCount; i++) {
CGPoint p=all.normalizedPoints[i];
[points addObject:PGProbePoint(CGPointMake(box.origin.x+p.x*box.size.width,
box.origin.y+p.y*box.size.height))];
}
NSDictionary *regions=@{@"contour":RegionIndices(lm.faceContour,all),
@"leftEye":RegionIndices(lm.leftEye,all), @"rightEye":RegionIndices(lm.rightEye,all),
@"leftPupil":RegionIndices(lm.leftPupil,all), @"rightPupil":RegionIndices(lm.rightPupil,all),
@"nose":RegionIndices(lm.nose,all), @"noseCrest":RegionIndices(lm.noseCrest,all),
@"outerLips":RegionIndices(lm.outerLips,all), @"innerLips":RegionIndices(lm.innerLips,all)};
[faces addObject:@{@"box":@[@(box.origin.x),@(box.origin.y),@(box.size.width),@(box.size.height)],
@"confidence":@(face.confidence), @"yaw":face.yaw?:NSNull.null,
@"roll":face.roll?:NSNull.null, @"pitch":face.pitch?:NSNull.null,
@"allPoints":points, @"regions":regions}];
}
NSString *name=[NSString stringWithFormat:@"frame-%0.3f.png",seconds];
NSURL *png=[[output URLByDeletingLastPathComponent] URLByAppendingPathComponent:name];
CGImageDestinationRef destination=CGImageDestinationCreateWithURL((__bridge CFURLRef)png,CFSTR("public.png"),1,NULL);
if (!destination) { CGImageRelease(image); return 1; }
CGImageDestinationAddImage(destination,image,NULL);
BOOL saved=CGImageDestinationFinalize(destination); CFRelease(destination);
[frames addObject:@{@"requestedSeconds":@(seconds), @"actualSeconds":@(CMTimeGetSeconds(actual)),
@"width":@(CGImageGetWidth(image)), @"height":@(CGImageGetHeight(image)),
@"image":name, @"faces":faces}];
CGImageRelease(image); if (!saved) return 1;
}}
NSDictionary *result=@{@"schema":@1,@"source":source.path,@"coordinateSystem":@"full-image normalized, y up",
@"authority":@"independent Apple Vision probe, not reference plug-in execution",
@"requestRevision":@3,@"constellation":@76,@"frames":frames};
NSData *json=[NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted|NSJSONWritingSortedKeys error:&error];
if (!json || ![json writeToURL:output options:NSDataWritingAtomic error:&error]) {
fprintf(stderr,"%s\n",error.description.UTF8String); return 1;
}
printf("Saved %lu independent frame observations.\n",(unsigned long)frames.count);
return 0;
}}
+219
查看文件
@@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""Encode/audit our own FxPlug custom value in observed Motion XML structure.
No host is started. A successful audit verifies structure and archive contents;
it does not establish Motion/FCP custom-view loading or native interaction.
"""
from __future__ import annotations
import argparse
import base64
import hashlib
import json
import math
from pathlib import Path
import plistlib
import re
import string
import struct
import sys
import xml.etree.ElementTree as ET
ALPHABET = "*-0123456789" + string.ascii_uppercase + string.ascii_lowercase
STANDARD = string.ascii_uppercase + string.ascii_lowercase + string.digits + "+/"
PLUGIN_UUID = "FB14B0E6-59B2-4836-916F-CD0C559E9241"
PLACEHOLDER_SHA256 = "a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e"
def require(condition: bool, message: str) -> None:
if not condition:
raise ValueError(message)
def digest(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def resolve(objects: list, value, seen: frozenset[int] = frozenset()):
"""Read Foundation collection values without instantiating archived classes."""
if isinstance(value, plistlib.UID):
index = value.data
require(0 <= index < len(objects) and index not in seen, "Invalid/cyclic archive UID")
return resolve(objects, objects[index], seen | {index})
if isinstance(value, list):
return [resolve(objects, item, seen) for item in value]
if isinstance(value, dict):
if "NS.keys" in value:
keys, items = value["NS.keys"], value["NS.objects"]
require(len(keys) == len(items), "Mismatched dictionary keys/objects")
return {resolve(objects, k, seen): resolve(objects, v, seen) for k, v in zip(keys, items)}
if "NS.objects" in value:
return resolve(objects, value["NS.objects"], seen)
return {k: resolve(objects, v, seen) for k, v in value.items() if k != "$class"}
return value
def inspect_inner(inner: bytes) -> dict:
archive = plistlib.loads(inner)
require(archive.get("$archiver") == "NSKeyedArchiver", "Inner value is not a keyed archive")
require(set(archive.get("$top", {})) == {"Custom Data"}, "Inner archive requires the Custom Data key")
objects = archive["$objects"]
state = objects[archive["$top"]["Custom Data"].data]
require(objects[state["$class"].data]["$classname"] == "PGInspectorState", "Unexpected custom class")
classes = sorted({x["$classname"] for x in objects if isinstance(x, dict) and "$classname" in x})
require(set(classes) <= {"PGInspectorState", "NSObject", "NSDictionary", "NSMutableDictionary", "NSSet", "NSMutableSet", "NSNumber"}, "Unexpected archived class")
decoded = resolve(objects, state)
require(decoded.get("schema") == 1 and isinstance(decoded.get("edited"), bool), "Unexpected state schema")
values = decoded.get("values")
require(isinstance(values, dict) and values, "Missing numeric control values")
require(all(isinstance(k, int) and not isinstance(k, bool) and isinstance(v, (int, float)) and math.isfinite(v) for k, v in values.items()), "Non-numeric control values")
disabled = decoded.get("disabled")
require(isinstance(disabled, list) and all(isinstance(v, int) for v in disabled), "Invalid disabled controls")
return {"class": "PGInspectorState", "schema": decoded["schema"], "hasEdits": decoded["edited"],
"values": values, "disabled": disabled, "classes": classes,
"innerBytes": len(inner), "innerSHA256": digest(inner)}
def wrap_inner(inner: bytes) -> bytes:
inspect_inner(inner)
# This is the observed NSKeyedArchiver wrapper, containing NSData and
# NSNumber(false), rather than a vendor custom object or copied blob.
return plistlib.dumps({"$version": 100000, "$archiver": "NSKeyedArchiver",
"$top": {"DataIsLegacy": plistlib.UID(2), "BlindDataObject": plistlib.UID(1)},
"$objects": ["$null", inner, False]}, fmt=plistlib.FMT_BINARY, sort_keys=False)
def encode_outer(outer: bytes) -> str:
data = struct.pack(">Q", len(outer)) + b"*" + outer
data += b"\0" * (-len(data) % 3)
return base64.b64encode(data).decode("ascii").translate(str.maketrans(STANDARD, ALPHABET))
def decode_text(text: str) -> tuple[bytes, bytes, dict]:
require(text and len(text) % 4 == 0 and set(text) <= set(ALPHABET), "Invalid Motion text encoding")
data = base64.b64decode(text.translate(str.maketrans(ALPHABET, STANDARD)), validate=True)
require(len(data) >= 9 and data[8] == 0x2A, "Unexpected Motion envelope")
length = int.from_bytes(data[:8], "big")
require(0 < length <= 16 * 1024 * 1024 and 0 <= len(data) - 9 - length <= 2, "Invalid payload length")
require(not any(data[9+length:]), "Nonzero envelope padding")
outer = data[9:9+length]
archive = plistlib.loads(outer)
require(archive.get("$archiver") == "NSKeyedArchiver", "Unexpected outer archive")
require(set(archive.get("$top", {})) == {"DataIsLegacy", "BlindDataObject"}, "Unexpected outer keys")
objects = archive["$objects"]
legacy = resolve(objects, archive["$top"]["DataIsLegacy"])
inner = resolve(objects, archive["$top"]["BlindDataObject"])
require(legacy is False and isinstance(inner, bytes), "Expected a nonlegacy NSData custom value")
require(encode_outer(outer) == text, "Text roundtrip mismatch")
state = inspect_inner(inner)
state.update({"outerBytes": len(outer), "outerSHA256": digest(outer), "encodedCharacters": len(text), "exactTextRoundtrip": True})
return outer, inner, state
def find_plugin(root: ET.Element) -> ET.Element:
matches = [x for x in root.iter("filter") if x.get("pluginUUID") == PLUGIN_UUID]
require(len(matches) == 1, "Expected exactly one product filter")
return matches[0]
def parameter(node: ET.Element, channel: str) -> ET.Element:
require(channel.startswith("./"), "Expected a relative parameter path")
for identifier in channel[2:].split("/"):
node = node.find(f"parameter[@id='{identifier}']")
require(node is not None, f"Unresolved parameter path {channel}")
return node
def audit(path: Path, parameters_path: Path | None = None) -> tuple[dict, bytes, bytes]:
root = ET.parse(path).getroot()
plugin = find_plugin(root)
custom = parameter(plugin, "./210")
require(custom.get("name") == "" and custom.get("flags") == "12901679104", "Custom entry differs from observed native structure")
require(custom.findtext("numberOfKeypoints") == "1" and custom.findtext("keypoint/time") == "0 0 0 0" and custom.find("keypoint/value") is not None, "Missing native custom keypoint skeleton")
text = custom.findtext("dataValue")
require(text == custom.findtext("defaultVal"), "Initial current/default custom values differ")
outer, inner, state = decode_text(text)
require(state["hasEdits"] is False, "Initial custom value must preserve legacy native settings")
entities = [x for x in root.iter() if x.tag in {"scenenode", "layer", "group", "filter", "clip", "footage"}]
ids = [x.get("id") for x in entities]
require(None not in ids and len(ids) == len(set(ids)), "Duplicate/missing entity identity")
by_id = dict(zip(ids, entities))
targets = root.findall("./scene/publishSettings/target")
require(len(targets) == 3, "Expected exactly three publications")
require([x.get("name") for x in targets] == ["FCP 版本作者→", "更多模板浏览器访问→", ""], "Unexpected publication order/names")
require(targets[2].get("object") == plugin.get("id") and targets[2].get("channel") == "./210", "Custom publication does not target this filter")
for item in targets:
require(item.get("object") in by_id, "Unresolved publication object")
parameter(by_id[item.get("object")], item.get("channel"))
for target, title in zip(targets[:2], ["大鱼先森Media", "dayu.media"]):
entry = parameter(by_id[target.get("object")], "./2/100")
require(len(entry.findall("entry")) == 1 and title in ET.tostring(entry, encoding="unicode"), "Copyright menu differs")
require(parameter(plugin, "./100/12").get("value") == "0.5", "Template midtone default was lost")
require(parameter(plugin, "./10005").get("value") == "0", "New template OSC must be off")
for group in range(100, 106):
parameter(plugin, f"./{group}")
for identifier in (45, 46, 50, 51, 52, 53):
parameter(plugin, f"./104/{identifier}")
for identifier in (60, 61, 62):
parameter(plugin, f"./{identifier}")
metadata = parameters_path or path.with_name("PARAMETERS.json")
listed = json.loads(metadata.read_text())
for item in listed["parameters"]:
parameter(plugin, item["channel"])
require(bool(item["published"]) == (item["id"] == 210), "Legacy flat entry is still published")
clips = root.findall(".//footage/clip")
require(len(clips) == 1 and clips[0].findtext("pathURL") == "Drop Zone.tiff", "Unexpected effect source media")
require(clips[0].get("id") == "510000006" and by_id["510000007"].find("clip") is clips[0], "Broken source clip/footage hierarchy")
source = by_id["510000004"]
require(parameter(source, "./1/324/300").get("value") == "510000006", "Broken Source Media reference")
require(parameter(source, "./2/321").get("value") == "3", "Effect Source type changed")
require(parameter(source, "./2/311").get("value") == "1", "Effect Source Drop Zone changed")
media = path.parent / "Drop Zone.tiff"
require(media.is_file() and digest(media.read_bytes()) == PLACEHOLDER_SHA256, "Placeholder missing/changed")
result = {"status": "structure_verified_native_loading_pending", "templateSHA256": digest(path.read_bytes()),
"pluginVersion": plugin.get("pluginVersion"), "publications": [dict(x.attrib) for x in targets],
"parameterPathsResolved": len(listed["parameters"]), "sourceMediaClosed": True,
"mediaSHA256": PLACEHOLDER_SHA256, "customState": state,
"nativeLoading": "pending user; no host executed"}
return result, outer, inner
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="command", required=True)
enc = sub.add_parser("encode", help="Wrap our own secure Custom Data archive and encode Motion text")
enc.add_argument("--inner", type=Path, required=True)
enc.add_argument("--text-output", type=Path, required=True)
enc.add_argument("--outer-output", type=Path)
chk = sub.add_parser("audit", help="Read-only template/archive/reference checks; native loading stays pending")
chk.add_argument("template", type=Path)
chk.add_argument("--parameters", type=Path)
chk.add_argument("--json-output", type=Path)
chk.add_argument("--inner-output", type=Path)
chk.add_argument("--outer-output", type=Path)
args = parser.parse_args()
if args.command == "encode":
inner = args.inner.read_bytes(); outer = wrap_inner(inner); text = encode_outer(outer)
decoded_outer, decoded_inner, result = decode_text(text)
require(decoded_inner == inner and decoded_outer == outer, "Own archive roundtrip failed")
args.text_output.write_text(text + "\n")
if args.outer_output:
args.outer_output.write_bytes(outer)
else:
result, outer, inner = audit(args.template, args.parameters)
if args.json_output:
args.json_output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
if args.inner_output:
args.inner_output.write_bytes(inner)
if args.outer_output:
args.outer_output.write_bytes(outer)
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except (ValueError, KeyError, IndexError, TypeError, OSError, ET.ParseError, plistlib.InvalidFileException) as error:
print(f"FAIL: {error}", file=sys.stderr)
sys.exit(1)
+2 -1
查看文件
@@ -15,8 +15,9 @@ xcrun clang "${compiler_extra[@]}" -fobjc-arc -fmodules -Wall -Wextra -Wno-unuse
-I "$plugin_dir" -F "$sdk_frameworks" -F "$runtime_frameworks" \ -I "$plugin_dir" -F "$sdk_frameworks" -F "$runtime_frameworks" \
-Wl,-rpath,"$runtime_frameworks" \ -Wl,-rpath,"$runtime_frameworks" \
"$plugin_dir/PGBeautyRenderer.m" "$plugin_dir/PGFaceProcessor.m" \ "$plugin_dir/PGBeautyRenderer.m" "$plugin_dir/PGFaceProcessor.m" \
"$plugin_dir/PGInspectorState.m" "$plugin_dir/PGInspectorView.m" \
"$plugin_dir/beatygrilPlugIn.m" tests/host_adapter_tests.m \ "$plugin_dir/beatygrilPlugIn.m" tests/host_adapter_tests.m \
-framework Foundation -framework CoreImage -framework CoreGraphics \ -framework Foundation -framework AppKit -framework CoreImage -framework CoreGraphics \
-framework CoreMedia -framework CoreVideo -framework IOSurface -framework Metal \ -framework CoreMedia -framework CoreVideo -framework IOSurface -framework Metal \
-framework Vision -framework ImageIO -framework FxPlug -framework PluginManager \ -framework Vision -framework ImageIO -framework FxPlug -framework PluginManager \
-o "$output_dir/host-tests" -o "$output_dir/host-tests"
+24
查看文件
@@ -0,0 +1,24 @@
#!/bin/zsh
set -euo pipefail
cd "${0:A:h:h}"
output_dir=$(mktemp -d /tmp/paintfcp-inspector-tests.XXXXXX)
trap 'rm -rf "$output_dir"' EXIT
plugin_dir="$PWD/beatygril/beatygril/Plugin"
compiler_extra=()
if [[ "${PAINTFCP_INSPECTOR_ASAN:-0}" == 1 ]]; then
compiler_extra=(-fsanitize=address -g)
fi
python3 scripts/inspector-template.py audit 'Templates/大鱼人像美化/大鱼人像美化.moef' \
--inner-output "$output_dir/template-inner.bplist" --outer-output "$output_dir/template-outer.bplist" > /dev/null
xcrun clang "${compiler_extra[@]}" -fobjc-arc -fmodules -Wall -Wextra -Wno-unused-parameter \
-DCI_SILENCE_GL_DEPRECATION -mmacosx-version-min=13.0 -I "$plugin_dir" \
-F /Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks -F /Library/Developer/Frameworks \
-Wl,-rpath,/Library/Developer/Frameworks \
"$plugin_dir/PGBeautyRenderer.m" "$plugin_dir/PGFaceProcessor.m" \
"$plugin_dir/PGInspectorState.m" "$plugin_dir/PGInspectorView.m" \
"$plugin_dir/beatygrilPlugIn.m" tests/inspector_tests.m \
-framework Foundation -framework AppKit -framework CoreImage -framework CoreGraphics \
-framework CoreMedia -framework CoreVideo -framework IOSurface -framework Metal \
-framework Vision -framework ImageIO -framework FxPlug -framework PluginManager \
-o "$output_dir/inspector-tests"
"$output_dir/inspector-tests" "$output_dir/template-inner.bplist" "$output_dir/template-outer.bplist"
+2
查看文件
@@ -9,3 +9,5 @@ xcrun clang "${common[@]}" "$plugin_dir/PGBeautyRenderer.m" tests/beauty_render_
"$output_dir/beauty-tests" "$output_dir/beauty-tests"
xcrun clang "${common[@]}" "$plugin_dir/PGFaceProcessor.m" tests/face_tests.m -o "$output_dir/face-tests" xcrun clang "${common[@]}" "$plugin_dir/PGFaceProcessor.m" tests/face_tests.m -o "$output_dir/face-tests"
"$output_dir/face-tests" "$PWD/tests/fixtures/astronaut.png" "$output_dir/face-tests" "$PWD/tests/fixtures/astronaut.png"
xcrun clang "${common[@]}" "$plugin_dir/PGFaceProcessor.m" tests/slim_tests.m -o "$output_dir/slim-tests"
"$output_dir/slim-tests" "$PWD/tests/fixtures/astronaut.png"
+4 -2
查看文件
@@ -88,7 +88,7 @@ static void RealtimeChecks(CIContext *context,CIImage *image) {
VNFaceObservation *left=Face(CGRectMake(.10,.25,.30,.50)); VNFaceObservation *left=Face(CGRectMake(.10,.25,.30,.50));
VNFaceObservation *right=Face(CGRectMake(.66,.30,.20,.40)); VNFaceObservation *right=Face(CGRectMake(.66,.30,.20,.40));
PGFaceFixture *p=[PGFaceFixture new];p.detections=@[left,right]; PGFaceFixture *p=[PGFaceFixture new];p.detections=@[left,right];
s.faceThin=.8; s.faceSmallHead=.8;
PGFaceFrame *automatic=[p processImage:image settings:s time:kCMTimeZero error:&error]; PGFaceFrame *automatic=[p processImage:image settings:s time:kCMTimeZero error:&error];
Check(automatic.deformationGeometry.count==1 && GeometryBox(automatic.deformationGeometry.firstObject).origin.x<.5,@"default realtime geometry chooses one area/center primary without analysis"); Check(automatic.deformationGeometry.count==1 && GeometryBox(automatic.deformationGeometry.firstObject).origin.x<.5,@"default realtime geometry chooses one area/center primary without analysis");
Check(automatic.faces.count==2 && automatic.faces[0]==left && automatic.faces[1]==right,@"default appearance retains both original Vision observations"); Check(automatic.faces.count==2 && automatic.faces[0]==left && automatic.faces[1]==right,@"default appearance retains both original Vision observations");
@@ -322,7 +322,9 @@ int main(int argc, const char *argv[]) {
float *controls[] = {&changed.faceThin, &changed.faceBigEye, &changed.faceShort, &changed.faceNarrow, &changed.faceChin, &changed.faceV, &changed.faceJaw, &changed.faceCheekbone, &changed.faceSmallHead}; float *controls[] = {&changed.faceThin, &changed.faceBigEye, &changed.faceShort, &changed.faceNarrow, &changed.faceChin, &changed.faceV, &changed.faceJaw, &changed.faceCheekbone, &changed.faceSmallHead};
*controls[i] = 1; *controls[i] = 1;
PGFaceFrame *warped = [processor processImage:image settings:changed time:kCMTimeZero error:&error]; PGFaceFrame *warped = [processor processImage:image settings:changed time:kCMTimeZero error:&error];
Check(warped != nil && ![Pixels(context, warped.image) isEqualToData:base], [@"geometry control changes pixels: " stringByAppendingString:names[i]]); BOOL changedPixels=![Pixels(context, warped.image) isEqualToData:base];
BOOL requiresPoints=(i==0 || i==4);
Check(warped != nil && (requiresPoints ? !changedPixels : changedPixels), requiresPoints ? @"thin/chin safely skip a face with no indexed landmarks" : [@"geometry control changes pixels: " stringByAppendingString:names[i]]);
Check(CGRectEqualToRect(warped.image.extent, image.extent), @"warp preserves image extent"); Check(CGRectEqualToRect(warped.image.extent, image.extent), @"warp preserves image extent");
} }
settings.faceShort = 0.8; settings.faceShort = 0.8;
+232 -5
查看文件
@@ -7,6 +7,10 @@
#import <ImageIO/ImageIO.h> #import <ImageIO/ImageIO.h>
#import "beatygrilPlugIn.h" #import "beatygrilPlugIn.h"
#import "PGParameterIDs.h" #import "PGParameterIDs.h"
#import "PGFaceProcessor.h"
#import "PGBeautyRenderer.h"
#import "PGInspectorState.h"
#include <math.h>
// This adapter simulates host callbacks, but uses real IOSurface buffers and the // This adapter simulates host callbacks, but uses real IOSurface buffers and the
// production FxPlug entry points. It does not simulate FCP plugin discovery/UI. // production FxPlug entry points. It does not simulate FCP plugin discovery/UI.
@@ -25,6 +29,9 @@
@property BOOL rejectAnalysis; @property BOOL rejectAnalysis;
@property BOOL targetAtCurrentTime; @property BOOL targetAtCurrentTime;
@property UInt32 rejectedWriteParameter; @property UInt32 rejectedWriteParameter;
@property NSUInteger actionDepth;
@property NSUInteger actionStarts;
@property NSUInteger actionEnds;
@end @end
@implementation PGHostAPI @implementation PGHostAPI
@@ -70,6 +77,9 @@
- (BOOL)addStringParameterWithName:(NSString *)name parameterID:(UInt32)identifier defaultValue:(NSString *)value parameterFlags:(FxParameterFlags)flags { - (BOOL)addStringParameterWithName:(NSString *)name parameterID:(UInt32)identifier defaultValue:(NSString *)value parameterFlags:(FxParameterFlags)flags {
return [self define:identifier kind:@"string" value:value flags:flags extra:@{@"name":name}]; return [self define:identifier kind:@"string" value:value flags:flags extra:@{@"name":name}];
} }
- (BOOL)addCustomParameterWithName:(NSString *)name parameterID:(UInt32)identifier defaultValue:(NSObject<NSSecureCoding,NSCopying> *)value parameterFlags:(FxParameterFlags)flags {
return [self define:identifier kind:@"custom" value:[value copy] flags:flags extra:@{@"name":name}];
}
- (BOOL)startParameterSubGroup:(NSString *)name parameterID:(UInt32)identifier parameterFlags:(FxParameterFlags)flags { - (BOOL)startParameterSubGroup:(NSString *)name parameterID:(UInt32)identifier parameterFlags:(FxParameterFlags)flags {
_depth++; _depth++;
return [self define:identifier kind:@"group" value:nil flags:flags extra:@{@"name":name}]; return [self define:identifier kind:@"group" value:nil flags:flags extra:@{@"name":name}];
@@ -101,6 +111,17 @@
_values[@(identifier)] = value; return YES; _values[@(identifier)] = value; return YES;
} }
- (BOOL)setBoolValue:(BOOL)value toParameter:(UInt32)identifier atTime:(CMTime)time { _lastSetBoolTime = time; _values[@(identifier)] = @(value); return YES; } - (BOOL)setBoolValue:(BOOL)value toParameter:(UInt32)identifier atTime:(CMTime)time { _lastSetBoolTime = time; _values[@(identifier)] = @(value); return YES; }
- (BOOL)getCustomParameterValue:(NSObject **)value fromParameter:(UInt32)identifier atTime:(CMTime)time {
NSObject *object=_values[@(identifier)];if(![object isKindOfClass:PGInspectorState.class])return NO;
*value=[(PGInspectorState *)object copy];return YES;
}
- (BOOL)setCustomParameterValue:(NSObject<NSSecureCoding,NSCopying> *)value toParameter:(UInt32)identifier atTime:(CMTime)time {
if(identifier==_rejectedWriteParameter)return NO;_values[@(identifier)]=[value copy];return YES;
}
- (BOOL)setFloatValue:(double)value toParameter:(UInt32)identifier atTime:(CMTime)time {_values[@(identifier)]=@(value);return YES;}
- (BOOL)setIntValue:(int)value toParameter:(UInt32)identifier atTime:(CMTime)time {_values[@(identifier)]=@(value);return YES;}
- (void)startAction:(id)sender {_actionStarts++;_actionDepth++;}
- (void)endAction:(id)sender {_actionEnds++;if(_actionDepth)_actionDepth--;}
- (CMTime)currentTime { return _hostTime; } - (CMTime)currentTime { return _hostTime; }
- (BOOL)startForwardAnalysis:(FxAnalysisLocation)location error:(NSError **)error { - (BOOL)startForwardAnalysis:(FxAnalysisLocation)location error:(NSError **)error {
_forwardRequests++; return !_rejectAnalysis; _forwardRequests++; return !_rejectAnalysis;
@@ -173,6 +194,12 @@ static NSUInteger MaximumDifference(NSData *a, NSData *b) {
for (NSUInteger i = 0; i < a.length; i++) maximum = MAX(maximum, (NSUInteger)abs(x[i] - y[i])); for (NSUInteger i = 0; i < a.length; i++) maximum = MAX(maximum, (NSUInteger)abs(x[i] - y[i]));
return maximum; return maximum;
} }
static double MeanDifference(NSData *a, NSData *b) {
if (!a.length || a.length != b.length) return INFINITY;
const uint8_t *x = a.bytes, *y = b.bytes; double total = 0;
for (NSUInteger i = 0; i < a.length; i++) total += abs(x[i]-y[i]);
return total/a.length;
}
static NSData *ExpectedCrop(PGHostTile *source, NSUInteger x, NSUInteger y, NSUInteger width, NSUInteger height, BOOL flip) { static NSData *ExpectedCrop(PGHostTile *source, NSUInteger x, NSUInteger y, NSUInteger width, NSUInteger height, BOOL flip) {
NSData *sourceBytes = SurfaceBytes(source); size_t sourceWidth = IOSurfaceGetWidth((__bridge IOSurfaceRef)source.storage); NSData *sourceBytes = SurfaceBytes(source); size_t sourceWidth = IOSurfaceGetWidth((__bridge IOSurfaceRef)source.storage);
NSMutableData *result = [NSMutableData dataWithLength:width * height * 4]; NSMutableData *result = [NSMutableData dataWithLength:width * height * 4];
@@ -200,6 +227,204 @@ static void RejectState(beatygrilPlugIn *plugin, PGHostTile *destination, PGHost
Check(!rendered && !escaped && error != nil, description); Check(!rendered && !escaped && error != nil, description);
} }
static NSData *PlistState(id value) { return [NSPropertyListSerialization dataWithPropertyList:value format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil]; } static NSData *PlistState(id value) { return [NSPropertyListSerialization dataWithPropertyList:value format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil]; }
// Decode independently of CI/IOSurface, so a pair of adapter flips cannot
// silently cancel in our test input and expected output. ImageIO + Quartz gives
// the NASA fixture top-first BGRA bytes, also cross-checked against Pillow.
static NSData *DecodedPortrait(NSString *path, size_t *width, size_t *height) {
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:path], NULL);
CGImageRef image = source ? CGImageSourceCreateImageAtIndex(source, 0, NULL) : NULL;
if (source) CFRelease(source);
if (!image) return nil;
*width = CGImageGetWidth(image); *height = CGImageGetHeight(image);
NSMutableData *bytes = [NSMutableData dataWithLength:*width * *height * 4];
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
CGContextRef context = CGBitmapContextCreate(bytes.mutableBytes, *width, *height, 8, *width * 4, space,
kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
if (context) {
CGContextSetBlendMode(context, kCGBlendModeCopy);
CGContextSetInterpolationQuality(context, kCGInterpolationNone);
CGContextDrawImage(context, CGRectMake(0, 0, *width, *height), image);
CGContextRelease(context);
}
CGColorSpaceRelease(space); CGImageRelease(image);
return context ? bytes : nil;
}
static BOOL StoreSurfaceBytes(PGHostTile *tile, NSData *bytes) {
IOSurfaceRef surface = (__bridge IOSurfaceRef)tile.storage;
size_t width = IOSurfaceGetWidth(surface), height = IOSurfaceGetHeight(surface);
if (bytes.length != width * height * 4) return NO;
IOSurfaceLock(surface, 0, NULL);
for (size_t row = 0; row < height; row++)
memcpy((uint8_t *)IOSurfaceGetBaseAddress(surface) + row * IOSurfaceGetBytesPerRow(surface),
(const uint8_t *)bytes.bytes + row * width * 4, width * 4);
IOSurfaceUnlock(surface, 0, NULL);
return YES;
}
static CIContext *NumericContext(void) {
return [CIContext contextWithMTLDevice:MTLCreateSystemDefaultDevice() options:@{
kCIContextWorkingColorSpace:NSNull.null, kCIContextOutputColorSpace:NSNull.null}];
}
static NSData *ImageBytes(CIContext *context, CIImage *image) {
if (!image) return nil;
size_t width = (size_t)image.extent.size.width, height = (size_t)image.extent.size.height;
NSMutableData *bytes = [NSMutableData dataWithLength:width * height * 4];
[context render:image toBitmap:bytes.mutableBytes rowBytes:width * 4 bounds:image.extent format:kCIFormatBGRA8 colorSpace:nil];
return bytes;
}
static CIImage *ProcessedReference(CIImage *image, PGBeautySettings settings, CMTime time, NSData *track) {
PGFaceProcessor *faces = [PGFaceProcessor new];
if (track) [faces setTrackingData:track forTarget:settings.targetIndex];
NSError *error = nil;
PGFaceFrame *frame = [faces processImage:image settings:settings time:time error:&error];
if (!frame) return nil;
return [[PGBeautyRenderer new] processImage:frame.image settings:settings skinMask:frame.skinMask
colorMask:frame.colorMask teethMask:frame.teethMask eyeMask:frame.eyeMask error:&error];
}
static CGPoint LandmarkCenter(VNFaceLandmarkRegion2D *region, CGRect box, CGSize size) {
CGPoint center = CGPointZero;
if (!region.pointCount || !region.normalizedPoints) return center;
for (NSUInteger i = 0; i < region.pointCount; i++) {
center.x += region.normalizedPoints[i].x; center.y += region.normalizedPoints[i].y;
}
center.x /= region.pointCount; center.y /= region.pointCount;
return CGPointMake((box.origin.x + center.x * box.size.width) * size.width,
(1 - box.origin.y - center.y * box.size.height) * size.height);
}
static double ChangeMass(NSData *baseline, NSData *changed, size_t width, CGRect region) {
if (baseline.length != changed.length || !width) return INFINITY;
const uint8_t *a = baseline.bytes, *b = changed.bytes;
size_t height = baseline.length / (width * 4); double total = 0;
for (size_t row = 0; row < height; row++) for (size_t x = 0; x < width; x++)
if (CGRectContainsPoint(region, CGPointMake(x + .5, row + .5))) {
size_t i = (row * width + x) * 4;
for (size_t c = 0; c < 3; c++) total += abs(a[i+c] - b[i+c]);
}
return total;
}
static void PortraitAnatomyTests(NSString *fixture, CIImage *portrait) {
size_t width = 0, height = 0;
NSData *canonical = DecodedPortrait(fixture, &width, &height);
Check(canonical != nil && width == 512 && height == 512, @"independent ImageIO/Quartz decodes the 512px NASA fixture");
if (!canonical || width != 512 || height != 512 || !portrait) return;
// Asymmetric samples pin the decoder's physical row/column order. These
// values match Pillow's RGB decode exactly, independently of Core Image.
const size_t positions[][2] = {{16,16},{100,250},{205,100},{222,178},{480,450}};
const uint8_t samples[][4] = {{35,6,14,255},{93,132,234,255},{11,35,58,255},{42,73,102,255},{2,1,3,255}};
BOOL samplesMatch = YES;
for (size_t i = 0; i < 5; i++)
samplesMatch &= memcmp((const uint8_t *)canonical.bytes + (positions[i][1]*width + positions[i][0])*4, samples[i], 4) == 0;
Check(samplesMatch, @"independent asymmetric pixels establish top-first BGRA and the horizontal direction");
CIContext *context = NumericContext();
Check(MaximumDifference(canonical, ImageBytes(context, portrait)) <= 1, @"upright CI file decode agrees with independent physical source bytes");
PGFaceFrame *detected = [[PGFaceProcessor new] processImage:portrait settings:PGBeautyDefaults() time:kCMTimeZero error:nil];
VNFaceObservation *face = detected.faces.firstObject;
Check(face.landmarks.allPoints.pointCount == 76, @"real portrait provides the 76-point anatomy used by chin pins");
if (face.landmarks.allPoints.pointCount != 76) return;
CGSize size = CGSizeMake(width, height); CGRect box = face.boundingBox;
CGPoint left = LandmarkCenter(face.landmarks.leftEye, box, size), right = LandmarkCenter(face.landmarks.rightEye, box, size);
CGPoint chin = face.landmarks.allPoints.normalizedPoints[67];
chin = CGPointMake((box.origin.x + chin.x * box.size.width)*width, (1-box.origin.y-chin.y*box.size.height)*height);
CGFloat fw = box.size.width*width, fh = box.size.height*height;
CGRect leftEye = CGRectMake(left.x-fw*.24, left.y-fh*.17, fw*.48, fh*.34);
CGRect rightEye = CGRectMake(right.x-fw*.24, right.y-fh*.17, fw*.48, fh*.34);
CGRect chinArea = CGRectMake(chin.x-fw*.30, chin.y-fh*.22, fw*.60, fh*.44);
CGRect upsideDownFace = CGRectMake(box.origin.x*width, box.origin.y*height, fw, fh);
const UInt32 controls[] = {0, PGBigEye, PGChin, PGChin};
const double values[] = {0, 1, 1, -1};
NSArray<NSString *> *names = @[@"default", @"big-eye", @"chin-positive", @"chin-negative"];
for (NSUInteger effect = 0; effect < 4; effect++) {
PGBeautySettings settings = PGBeautyDefaults();
if (effect == 1) settings.faceBigEye = 1;
else if (effect > 1) settings.faceChin = values[effect];
NSData *expected = ImageBytes(context, ProcessedReference(portrait, settings, kCMTimeZero, nil));
Check(expected.length == canonical.length, [names[effect] stringByAppendingString:@" independent upright face/beauty chain renders"]);
if (expected.length != canonical.length) continue;
NSData *originBaseline = nil;
for (NSUInteger sourceTop = 0; sourceTop < 2; sourceTop++) for (NSUInteger destinationTop = 0; destinationTop < 2; destinationTop++) {
@autoreleasepool {
PGHostAPI *api = [PGHostAPI new]; beatygrilPlugIn *plugin = [[beatygrilPlugIn alloc] initWithAPIManager:api];
Check([plugin addParametersWithError:nil], @"anatomy case creates the real plugin parameters");
if (effect) api.values[@(controls[effect])] = @(values[effect]);
PGHostTile *input = Tile(width, height, 37, -21, sourceTop ? kFxImageOrigin_TOP_LEFT : kFxImageOrigin_BOTTOM_LEFT);
PGHostTile *output = Tile(width, height, 37, -21, destinationTop ? kFxImageOrigin_TOP_LEFT : kFxImageOrigin_BOTTOM_LEFT);
// imageOrigin changes coordinate semantics, never these bytes.
Check(StoreSurfaceBytes(input, canonical), @"all origin combinations receive identical independently decoded physical rows");
Check(Render(plugin, output, @[input], Snapshot(plugin, kCMTimeZero), kCMTimeZero, nil, NULL), @"active anatomy case renders with nonzero tile origin");
NSData *actual = SurfaceBytes(output); NSUInteger difference = MaximumDifference(actual, expected);
NSString *label = [NSString stringWithFormat:@"%@ source%@/destination%@", names[effect], sourceTop?@"TOP":@"BOTTOM", destinationTop?@"TOP":@"BOTTOM"];
// Vision's file-image vs IOSurface path and translated float
// coordinates can round by a few byte levels at high contrast.
// Bound that error tightly; changing only origin tags must be
// exactly invariant, independently of this reference tolerance.
Check(difference <= 4 && MeanDifference(actual,expected) < .005, [label stringByAppendingString:@" matches independent upright CI output"]);
if (!originBaseline) originBaseline = actual;
Check(MaximumDifference(actual,originBaseline) == 0, [label stringByAppendingString:@" is byte-identical across coordinate tags"]);
double lm = ChangeMass(canonical, actual, width, leftEye), rm = ChangeMass(canonical, actual, width, rightEye);
double cm = ChangeMass(canonical, actual, width, chinArea), um = ChangeMass(canonical, actual, width, upsideDownFace);
BOOL located = effect == 0 ? MaximumDifference(canonical, actual) <= 1 :
(effect == 1 ? lm > 100 && rm > 100 && cm < (lm+rm)*.01 && um == 0 :
cm > 100 && cm > 4*(lm+rm) && um == 0);
Check(located, [label stringByAppendingString:effect == 1 ? @" changes the two real eye regions" : (effect ? @" changes the real chin rather than eyes or mirrored anatomy" : @" preserves the independently decoded photo")]);
printf("Anatomy %-13s %lu/%lu max=%lu mean=%.7f eye=%.0f/%.0f chin=%.0f inverted=%.0f\n", names[effect].UTF8String,
(unsigned long)sourceTop, (unsigned long)destinationTop, (unsigned long)difference, MeanDifference(actual,expected), lm, rm, cm, um);
}
}
}
}
static void TrackingCoordinateTests(NSDictionary *archive, NSDictionary *hostValues, PGHostTile *source, CIImage *portrait, CMTime time) {
Check([archive[@"hostCoordinateSpace"] isEqual:@"iosurface-ci-y-up-v1"], @"new host analysis stamps its explicit IOSurface/CI coordinate contract");
NSMutableDictionary *forged = [NSPropertyListSerialization propertyListWithData:PlistState(archive)
options:NSPropertyListMutableContainers format:nil error:nil];
if (![forged[@"frames"] isKindOfClass:NSDictionary.class]) { Check(NO, @"analysis archive supplies frames for compatibility regression"); return; }
// Retain fully valid topology but move the saved face away from the actual
// portrait. A falsely replayed legacy archive must produce a visible error.
for (NSMutableDictionary *frame in [forged[@"frames"] allValues]) for (NSMutableDictionary *face in frame[@"faces"]) {
NSMutableArray *box = [face[@"box"] mutableCopy]; box[0] = @([box[0] doubleValue]+.2); box[1] = @([box[1] doubleValue]-.25); face[@"box"] = box;
for (NSString *name in @[@"leftEye",@"rightEye",@"leftBrow",@"rightBrow",@"nose",@"lips",@"mouth",@"contour",@"points76"]) {
if (!face[name]) continue;
NSMutableArray *points = [NSMutableArray new];
for (NSArray *point in face[name]) [points addObject:@[@([point[0] doubleValue]+.2),@([point[1] doubleValue]-.25)]];
face[name] = points;
}
}
[forged removeObjectForKey:@"hostCoordinateSpace"];
NSString *legacyString = [PlistState(forged) base64EncodedStringWithOptions:0];
NSMutableDictionary *values = [hostValues mutableCopy];
values[@(PGTarget)] = @2; values[@(PGTrackEnabled)] = @YES; values[@(PGBigEye)] = @1; values[@(PGChin)] = @1;
values[@(PGTrackData3)] = legacyString;
PGHostAPI *legacyAPI = [PGHostAPI new]; legacyAPI.hostTime = time;
beatygrilPlugIn *legacy = [[beatygrilPlugIn alloc] initWithAPIManager:legacyAPI]; [legacy addParametersWithError:nil]; legacyAPI.values = [values mutableCopy];
PGHostTile *output = Tile(512,512,0,0,kFxImageOrigin_TOP_LEFT);
Check(Render(legacy, output, @[source], Snapshot(legacy,time), time, nil, NULL), @"unmarked old coordinate archive renders through the real adapter");
NSData *legacyBytes = SurfaceBytes(output);
PGBeautySettings realtime = [legacy settingsAtTime:time]; realtime.trackingEnabled = 0;
CIContext *context = NumericContext();
NSData *expectedLive = ImageBytes(context, ProcessedReference(portrait,realtime,time,nil));
printf("Legacy fallback upright comparison: max=%lu mean=%.7f\n",(unsigned long)MaximumDifference(legacyBytes,expectedLive),MeanDifference(legacyBytes,expectedLive));
Check(expectedLive.length && MaximumDifference(legacyBytes,expectedLive) <= 4 && MeanDifference(legacyBytes,expectedLive) < .005, @"unmarked forged face geometry falls back to realtime upright detection");
Check([legacyAPI.values[@(PGTrackData3)] isEqualToString:legacyString] && [legacyAPI.values[@(PGTrackEnabled)] boolValue], @"legacy fallback preserves the user's original archive string and enabled parameter");
forged[@"hostCoordinateSpace"] = @"iosurface-ci-y-up-v1";
NSData *trustedTrack = PlistState(forged);
PGHostAPI *trustedAPI = [PGHostAPI new]; trustedAPI.hostTime = time;
beatygrilPlugIn *trusted = [[beatygrilPlugIn alloc] initWithAPIManager:trustedAPI]; [trusted addParametersWithError:nil];
trustedAPI.values = [values mutableCopy]; trustedAPI.values[@(PGTrackData3)] = [trustedTrack base64EncodedStringWithOptions:0];
Check(Render(trusted, output, @[source], Snapshot(trusted,time), time, nil, NULL), @"fresh plugin accepts a snapshot with marked legacy analysis data");
NSData *trustedBytes = SurfaceBytes(output);
PGBeautySettings tracked = [trusted settingsAtTime:time]; tracked.trackingEnabled=1;
NSData *expectedSaved = ImageBytes(context, ProcessedReference(portrait,tracked,time,trustedTrack));
Check(expectedSaved.length && MaximumDifference(trustedBytes,expectedLive) <= 4 && MeanDifference(trustedBytes,expectedLive) < .005, @"automatic adapter ignores even marked saved geometry and uses the current face");
Check(MaximumDifference(expectedSaved,expectedLive) > 5, @"direct archive replay would visibly differ, making the automatic non-replay assertion meaningful");
// Reanalysis of an unmarked target starts a fresh compatible archive.
[legacy analyzeForward];
CMTime next = CMTimeAdd(time,CMTimeMake(10,24));
[legacy setupAnalysisForTimeRange:CMTimeRangeMake(time,CMTimeMake(240,24)) frameDuration:CMTimeMake(1,24) error:nil];
BOOL analyzed = [legacy analyzeFrame:source atTime:next error:nil] && [legacy cleanupAnalysis:nil];
NSData *resaved = [[NSData alloc] initWithBase64EncodedString:legacyAPI.values[@(PGTrackData3)] options:0];
NSDictionary *fresh = resaved ? [NSPropertyListSerialization propertyListWithData:resaved options:0 format:nil error:nil] : nil;
Check(analyzed && [fresh[@"frames"] count] == 1 && [fresh[@"hostCoordinateSpace"] isEqual:@"iosurface-ci-y-up-v1"], @"reanalysis replaces incompatible geometry with one newly marked frame");
}
static void WritePortrait(PGHostTile *tile, CIImage *image) { static void WritePortrait(PGHostTile *tile, CIImage *image) {
CIContext *context = [CIContext contextWithOptions:@{kCIContextWorkingColorSpace:NSNull.null, kCIContextOutputColorSpace:NSNull.null}]; CIContext *context = [CIContext contextWithOptions:@{kCIContextWorkingColorSpace:NSNull.null, kCIContextOutputColorSpace:NSNull.null}];
[context render:image toIOSurface:(__bridge IOSurfaceRef)tile.storage bounds:image.extent colorSpace:nil]; [context render:image toIOSurface:(__bridge IOSurfaceRef)tile.storage bounds:image.extent colorSpace:nil];
@@ -230,7 +455,7 @@ int main(int argc, const char *argv[]) {
PGHostAPI *api = [PGHostAPI new]; beatygrilPlugIn *plugin = [[beatygrilPlugIn alloc] initWithAPIManager:api]; PGHostAPI *api = [PGHostAPI new]; beatygrilPlugIn *plugin = [[beatygrilPlugIn alloc] initWithAPIManager:api];
NSError *error = nil; NSError *error = nil;
Check([plugin addParametersWithError:&error] && !error, @"host creates complete parameter set"); Check([plugin addParametersWithError:&error] && !error, @"host creates complete parameter set");
UInt32 identifiers[] = {PGAmount,PGBrightness,PGRadius,PGSharpness,PGMix,PGThreshold,PGSoftness,PGEdgeProtect,PGShowMask,PGMidtone,PGHue,PGSaturation,PGHighPass,PGThin,PGFaceBoxes,PGSingleFace,PGSelection,PGShort,PGNarrow,PGChin,PGVFace,PGJaw,PGCheekbone,PGSmallHead,PGSmallHeadRange,PGFeatureProtect,PGTeeth,PGEyeBright,PGBigEye,PGTrackBoxes,PGFastMulti,PGJumpGuard,PGEvenTone,PGTonePreset,PGInvertMask,PGDebugChannel,PGTrackEnabled,PGTarget,PGAnalyzeForward,PGAnalyzeBackward,PGClearTrack,PGStatus,PGTrackData1,PGTrackData2,PGTrackData3,PGSmoothingEnabled,PGColorEnabled,PGShapeEnabled,PGDetailEnabled}; UInt32 identifiers[] = {PGAmount,PGBrightness,PGRadius,PGSharpness,PGMix,PGThreshold,PGSoftness,PGEdgeProtect,PGShowMask,PGMidtone,PGHue,PGSaturation,PGHighPass,PGThin,PGFaceBoxes,PGSingleFace,PGSelection,PGShort,PGNarrow,PGChin,PGVFace,PGJaw,PGCheekbone,PGSmallHead,PGSmallHeadRange,PGFeatureProtect,PGTeeth,PGEyeBright,PGBigEye,PGTrackBoxes,PGFastMulti,PGJumpGuard,PGEvenTone,PGTonePreset,PGInvertMask,PGDebugChannel,PGTrackEnabled,PGTarget,PGAnalyzeForward,PGAnalyzeBackward,PGClearTrack,PGStatus,PGTrackData1,PGTrackData2,PGTrackData3,PGSmoothingEnabled,PGColorEnabled,PGShapeEnabled,PGDetailEnabled,PGInspectorPanel};
for (NSUInteger i = 0; i < sizeof(identifiers) / sizeof(UInt32); i++) Check(api.definitions[@(identifiers[i])] != nil, [NSString stringWithFormat:@"required parameter %u exists", identifiers[i]]); for (NSUInteger i = 0; i < sizeof(identifiers) / sizeof(UInt32); i++) Check(api.definitions[@(identifiers[i])] != nil, [NSString stringWithFormat:@"required parameter %u exists", identifiers[i]]);
Check(api.depth == 0 && !api.duplicate && api.definitions.count == sizeof(identifiers) / sizeof(UInt32) + 6, @"parameter groups close and IDs are unique"); Check(api.depth == 0 && !api.duplicate && api.definitions.count == sizeof(identifiers) / sizeof(UInt32) + 6, @"parameter groups close and IDs are unique");
Check([api.values[@(PGMidtone)] doubleValue] == 0 && [api.values[@(PGMix)] doubleValue] == 1, @"default detail and mix form identity filter"); Check([api.values[@(PGMidtone)] doubleValue] == 0 && [api.values[@(PGMix)] doubleValue] == 1, @"default detail and mix form identity filter");
@@ -283,7 +508,7 @@ int main(int argc, const char *argv[]) {
Check(MaximumDifference(SurfaceBytes(input), SurfaceBytes(output)) <= 1, top ? @"top-left tile preserves sample rows at nonzero origin" : @"bottom-left tile preserves sample rows at nonzero origin"); Check(MaximumDifference(SurfaceBytes(input), SurfaceBytes(output)) <= 1, top ? @"top-left tile preserves sample rows at nonzero origin" : @"bottom-left tile preserves sample rows at nonzero origin");
output.origin = top ? kFxImageOrigin_BOTTOM_LEFT : kFxImageOrigin_TOP_LEFT; output.origin = top ? kFxImageOrigin_BOTTOM_LEFT : kFxImageOrigin_TOP_LEFT;
Check(Render(plugin, output, @[input], state, kCMTimeZero, &error, NULL), @"opposite-origin tile renders"); Check(Render(plugin, output, @[input], state, kCMTimeZero, &error, NULL), @"opposite-origin tile renders");
Check(MaximumDifference(ExpectedCrop(input, 0, 0, 48, 32, YES), SurfaceBytes(output)) <= 1, @"opposite image origins flip rows exactly once"); Check(MaximumDifference(SurfaceBytes(input), SurfaceBytes(output)) <= 1, @"opposite coordinate tags preserve the same physical IOSurface rows");
} }
PGHostTile *full = Tile(48, 32, 37, -21, kFxImageOrigin_BOTTOM_LEFT), *crop = Tile(16, 12, 43, -17, kFxImageOrigin_BOTTOM_LEFT); Fill(full); PGHostTile *full = Tile(48, 32, 37, -21, kFxImageOrigin_BOTTOM_LEFT), *crop = Tile(16, 12, 43, -17, kFxImageOrigin_BOTTOM_LEFT); Fill(full);
Check(Render(plugin, crop, @[full], state, kCMTimeZero, &error, NULL), @"destination sub-tile renders from full input buffer"); Check(Render(plugin, crop, @[full], state, kCMTimeZero, &error, NULL), @"destination sub-tile renders from full input buffer");
@@ -298,11 +523,12 @@ int main(int argc, const char *argv[]) {
Check(cropDifference <= 1, @"destination sub-tile translates global pixel bounds correctly"); Check(cropDifference <= 1, @"destination sub-tile translates global pixel bounds correctly");
full.origin = kFxImageOrigin_TOP_LEFT; crop.origin = kFxImageOrigin_TOP_LEFT; full.origin = kFxImageOrigin_TOP_LEFT; crop.origin = kFxImageOrigin_TOP_LEFT;
Check(Render(plugin, crop, @[full], state, kCMTimeZero, &error, NULL), @"top-left destination sub-tile renders"); Check(Render(plugin, crop, @[full], state, kCMTimeZero, &error, NULL), @"top-left destination sub-tile renders");
Check(MaximumDifference(ExpectedCrop(full, 6, 4, 16, 12, NO), SurfaceBytes(crop)) <= 1, @"top-left sub-tile selects correct physical source rows"); Check(MaximumDifference(ExpectedCrop(full, 6, 16, 16, 12, NO), SurfaceBytes(crop)) <= 1, @"top-left coordinate tag does not reverse the sub-tile's physical source rows");
NSString *fixture = argc > 1 ? @(argv[1]) : @"tests/fixtures/astronaut.png"; NSString *fixture = argc > 1 ? @(argv[1]) : @"tests/fixtures/astronaut.png";
CIImage *portrait = [CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:fixture] options:@{kCIImageColorSpace:NSNull.null}]; CIImage *portrait = [CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:fixture] options:@{kCIImageColorSpace:NSNull.null}];
Check(portrait != nil, @"NASA portrait opens for full adapter test"); Check(portrait != nil, @"NASA portrait opens for full adapter test");
PortraitAnatomyTests(fixture, portrait);
PGHostTile *portraitSource = Tile(512, 512, 0, 0, kFxImageOrigin_BOTTOM_LEFT), *portraitOutput = Tile(512, 512, 0, 0, kFxImageOrigin_BOTTOM_LEFT); PGHostTile *portraitSource = Tile(512, 512, 0, 0, kFxImageOrigin_BOTTOM_LEFT), *portraitOutput = Tile(512, 512, 0, 0, kFxImageOrigin_BOTTOM_LEFT);
WritePortrait(portraitSource, portrait); WritePortrait(portraitSource, portrait);
api.values[@(PGThin)] = @0.8; api.values[@(PGBigEye)] = @0.6; api.values[@(PGMidtone)] = @0.8; api.values[@(PGBrightness)] = @0.25; api.values[@(PGMix)] = @0; api.values[@(PGThin)] = @0.8; api.values[@(PGBigEye)] = @0.6; api.values[@(PGMidtone)] = @0.8; api.values[@(PGBrightness)] = @0.25; api.values[@(PGMix)] = @0;
@@ -319,7 +545,7 @@ int main(int argc, const char *argv[]) {
api.values[@(PGSelection)] = @[@0.43, @0.76]; api.values[@(PGSelection)] = @[@0.43, @0.76];
NSData *oldRenderState = Snapshot(plugin, api.hostTime); NSData *oldRenderState = Snapshot(plugin, api.hostTime);
[plugin analyzeForward]; [plugin analyzeForward];
Check(api.forwardRequests == 1, @"forward analyze button calls host analysis API"); Check(api.forwardRequests == 1, @"hidden legacy forward-analysis method still accepts old document callbacks");
CMTimeRange desired, clip = CMTimeRangeMake(kCMTimeZero, CMTimeMake(240, 24)); CMTimeRange desired, clip = CMTimeRangeMake(kCMTimeZero, CMTimeMake(240, 24));
[plugin desiredAnalysisTimeRange:&desired forInputWithTimeRange:clip error:&error]; [plugin desiredAnalysisTimeRange:&desired forInputWithTimeRange:clip error:&error];
Check(CMTimeCompare(desired.start, api.hostTime) == 0 && CMTimeCompare(CMTimeRangeGetEnd(desired), CMTimeRangeGetEnd(clip)) == 0, @"analysis seed comes from current host playhead time"); Check(CMTimeCompare(desired.start, api.hostTime) == 0 && CMTimeCompare(CMTimeRangeGetEnd(desired), CMTimeRangeGetEnd(clip)) == 0, @"analysis seed comes from current host playhead time");
@@ -339,10 +565,11 @@ int main(int argc, const char *argv[]) {
NSData *tracking = [[NSData alloc] initWithBase64EncodedString:target options:0]; NSData *tracking = [[NSData alloc] initWithBase64EncodedString:target options:0];
NSDictionary *trackingArchive = tracking ? [NSPropertyListSerialization propertyListWithData:tracking options:0 format:nil error:nil] : nil; NSDictionary *trackingArchive = tracking ? [NSPropertyListSerialization propertyListWithData:tracking options:0 format:nil error:nil] : nil;
Check([trackingArchive[@"frames"] count] == 2, @"old render snapshot does not erase either newly analyzed frame"); Check([trackingArchive[@"frames"] count] == 2, @"old render snapshot does not erase either newly analyzed frame");
TrackingCoordinateTests(trackingArchive,api.values,portraitSource,portrait,api.hostTime);
Check([api.values[@(PGTrackEnabled)] boolValue] && CMTimeCompare(api.lastSetBoolTime, api.hostTime) == 0, @"analysis enables saved target at host seed time"); Check([api.values[@(PGTrackEnabled)] boolValue] && CMTimeCompare(api.lastSetBoolTime, api.hostTime) == 0, @"analysis enables saved target at host seed time");
NSData *trackedState = Snapshot(plugin, api.hostTime); NSData *trackedState = Snapshot(plugin, api.hostTime);
beatygrilPlugIn *restoredPlugin = [[beatygrilPlugIn alloc] initWithAPIManager:api]; beatygrilPlugIn *restoredPlugin = [[beatygrilPlugIn alloc] initWithAPIManager:api];
Check(Render(restoredPlugin, portraitOutput, @[portraitSource], trackedState, api.hostTime, &error, NULL), @"fresh plugin instance restores target data from serialized host snapshot"); Check(Render(restoredPlugin, portraitOutput, @[portraitSource], trackedState, api.hostTime, &error, NULL), @"fresh plugin instance uses automatic follow despite serialized legacy analysis settings");
[plugin analyzeBackward]; [plugin analyzeBackward];
Check(api.backwardRequests == 1, @"backward analyze button calls host API"); Check(api.backwardRequests == 1, @"backward analyze button calls host API");
[plugin desiredAnalysisTimeRange:&desired forInputWithTimeRange:clip error:&error]; [plugin desiredAnalysisTimeRange:&desired forInputWithTimeRange:clip error:&error];
+161
查看文件
@@ -0,0 +1,161 @@
#define main PGExistingHostAdapterMain
#include "host_adapter_tests.m"
#undef main
#import "PGInspectorView.h"
@interface PGInspectorTestAPI : PGHostAPI
@property NSUInteger accessViolations;
@property NSUInteger customWrites;
@property NSUInteger scalarWrites;
@property (strong) NSDictionary *actionBefore;
@property (strong) NSDictionary *undoValues;
@end
@implementation PGInspectorTestAPI
- (void)guard {if(!self.actionDepth)_accessViolations++;}
- (void)startAction:(id)sender {if(!self.actionDepth)_actionBefore=[self.values copy];[super startAction:sender];}
- (void)endAction:(id)sender {if(self.actionDepth==1 && ![self.values isEqual:_actionBefore])_undoValues=_actionBefore;[super endAction:sender];}
- (CMTime)currentTime {[self guard];return [super currentTime];}
- (BOOL)getCustomParameterValue:(NSObject **)v fromParameter:(UInt32)i atTime:(CMTime)t {[self guard];return [super getCustomParameterValue:v fromParameter:i atTime:t];}
- (BOOL)getFloatValue:(double *)v fromParameter:(UInt32)i atTime:(CMTime)t {[self guard];return [super getFloatValue:v fromParameter:i atTime:t];}
- (BOOL)getBoolValue:(BOOL *)v fromParameter:(UInt32)i atTime:(CMTime)t {[self guard];return [super getBoolValue:v fromParameter:i atTime:t];}
- (BOOL)getIntValue:(int *)v fromParameter:(UInt32)i atTime:(CMTime)t {[self guard];return [super getIntValue:v fromParameter:i atTime:t];}
- (BOOL)setCustomParameterValue:(NSObject<NSSecureCoding,NSCopying> *)v toParameter:(UInt32)i atTime:(CMTime)t {[self guard];_customWrites++;return [super setCustomParameterValue:v toParameter:i atTime:t];}
- (BOOL)setFloatValue:(double)v toParameter:(UInt32)i atTime:(CMTime)t {[self guard];_scalarWrites++;return [super setFloatValue:v toParameter:i atTime:t];}
- (BOOL)setBoolValue:(BOOL)v toParameter:(UInt32)i atTime:(CMTime)t {[self guard];_scalarWrites++;return [super setBoolValue:v toParameter:i atTime:t];}
- (BOOL)setIntValue:(int)v toParameter:(UInt32)i atTime:(CMTime)t {[self guard];_scalarWrites++;return [super setIntValue:v toParameter:i atTime:t];}
@end
static NSControl *FindControl(NSView *view,Class type,NSString *label) {
if([view isKindOfClass:type] && [view.accessibilityLabel isEqual:label])return (NSControl *)view;
for(NSView *child in view.subviews){NSControl *found=FindControl(child,type,label);if(found)return found;}return nil;
}
static void Fire(NSControl *control) {[control sendAction:control.action to:control.target];}
static BOOL Near(double a,double b) {return fabs(a-b)<.00001;}
static void RegistrationTests(void) {
PGHostAPI *api=[PGHostAPI new];beatygrilPlugIn *plugin=[[beatygrilPlugIn alloc]initWithAPIManager:api];[plugin addParametersWithError:nil];
NSUInteger visible=0;for(NSNumber *key in api.definitions){NSDictionary *d=api.definitions[key];if(!([d[@"flags"]unsignedIntValue]&kFxParameterFlag_HIDDEN)){visible++;Check(key.unsignedIntValue==PGInspectorPanel,@"only the custom panel is visible in the native parameter list");}}
Check(visible==1,@"all duplicate standard controls and manual-analysis groups are hidden");
FxParameterFlags flags=[api.definitions[@(PGInspectorPanel)][@"flags"]unsignedIntValue];
Check((flags&kFxParameterFlag_CUSTOM_UI) && (flags&kFxParameterFlag_USE_FULL_VIEW_WIDTH),@"panel requests full-width custom UI");
Check([plugin classForCustomParameterID:PGInspectorPanel]==PGInspectorState.class && [[plugin classesForCustomParameterID:PGInspectorPanel]containsObject:NSDictionary.class],@"host can securely decode the published state and its nested classes");
for(NSNumber *key in @[@(PGAnalyzeForward),@(PGAnalyzeBackward),@(PGClearTrack)])Check(([api.definitions[key][@"flags"]unsignedIntValue]&(kFxParameterFlag_HIDDEN|kFxParameterFlag_DISABLED))==(kFxParameterFlag_HIDDEN|kFxParameterFlag_DISABLED),@"legacy analysis commands are hidden and disabled");
Check([plugin createViewForParameterID:999]==nil,@"unknown parameter IDs do not create a second inspector");
for(NSDictionary *d in PGInspectorControls()) {
NSNumber *key=d[@"id"];Check(api.definitions[key]!=nil,@"every inspector control maps to an existing stable parameter ID");
if([d[@"kind"]isEqual:@"slider"])Check(Near([d[@"min"]doubleValue],[api.definitions[key][@"min"]doubleValue]) && Near([d[@"max"]doubleValue],[api.definitions[key][@"max"]doubleValue]),@"UI and renderer enforce the same slider range");
}
api.values[@(PGTrackEnabled)]=@YES;api.values[@(PGSingleFace)]=@YES;api.values[@(PGShowMask)]=@YES;
PGBeautySettings s=[plugin settingsAtTime:kCMTimeZero];Check(!s.trackingEnabled && !s.singleFace && !s.showMask,@"old manual analysis, selection and debug flags cannot override automatic output");
NSData *snapshot=Snapshot(plugin,kCMTimeZero);NSDictionary *plist=[NSPropertyListSerialization propertyListWithData:snapshot options:0 format:nil error:nil];
Check([plist[@"tracks"]isEqual:@[@"",@"",@""]],@"automatic render snapshots do not transfer persisted manual-analysis payloads");
}
static void ModelTests(void) {
PGInspectorState *plain=[PGInspectorState stateWithValues:@{} disabledParameters:[NSSet set] hasEdits:NO];
PGBeautySettings original=PGBeautyDefaults();original.midtoneDetail=.5;original.faceChin=-.3;
PGBeautySettings untouched=PGSettingsWithInspectorState(original,plain);
Check(memcmp(&original,&untouched,sizeof(original))==0,@"untouched panel preserves a legacy document's exact settings");
NSMutableDictionary *values=[plain.values mutableCopy];values[@(PGChin)]=@(-.6);values[@(PGBigEye)]=@.2;values[@(PGBrightness)]=@.37;values[@(PGTonePreset)]=@4;values[@(PGMidtone)]=@.7;
PGInspectorState *a=[PGInspectorState stateWithValues:values disabledParameters:[NSSet setWithObject:@(PGBrightness)] hasEdits:YES];
NSData *encoded=[NSKeyedArchiver archivedDataWithRootObject:a requiringSecureCoding:YES error:nil];
PGInspectorState *decoded=[NSKeyedUnarchiver unarchivedObjectOfClass:PGInspectorState.class fromData:encoded error:nil];
Check([decoded isEqual:a] && decoded!=a && decoded.hasEdits,@"secure host archive restores values, per-control bypass and edit state");
Check([[a copy]isEqual:a] && a.hash==[[a copy]hash],@"copy and equality are stable for host undo snapshots");
PGBeautySettings s=PGSettingsWithInspectorState(PGBeautyDefaults(),decoded);
Check(Near(s.faceChin,-.6) && Near(s.faceBigEye,.2) && s.skinBrightness==0 && s.skinTonePreset==4,@"chin, eyes and bypass route to the correct independent render fields");
NSMutableDictionary *bValues=[values mutableCopy];bValues[@(PGChin)]=@.6;bValues[@(PGTonePreset)]=@6;
PGInspectorState *b=[PGInspectorState stateWithValues:bValues disabledParameters:[NSSet set] hasEdits:YES];
PGInspectorState *mid=(PGInspectorState *)[a interpolateBetween:b withWeight:.5];
Check(Near([mid valueForParameter:PGChin],0) && [mid valueForParameter:PGTonePreset]==4 && ![mid isParameterEnabled:PGBrightness],@"numeric keyframes interpolate while menu and enable states hold the preceding key");
Check([[a interpolateBetween:b withWeight:0]isEqual:a] && [[a interpolateBetween:b withWeight:1]isEqual:b],@"keyframe endpoints restore exact independent states");
Check([[plain interpolateBetween:b withWeight:.5]isEqual:plain],@"uncommitted legacy state does not invent a numeric transition before the first panel edit");
NSDictionary *bad=@{@(PGChin):@9,@(PGTonePreset):@(-2),@(PGBrightness):@(NAN),@999:@.5};
PGInspectorState *safe=[PGInspectorState stateWithValues:bad disabledParameters:[NSSet setWithObjects:@(PGRadius),@999,nil] hasEdits:YES];
Check([safe valueForParameter:PGChin]==1 && [safe valueForParameter:PGTonePreset]==0 && [safe valueForParameter:PGBrightness]==0 && safe.values[@999]==nil && safe.disabledParameters.count==0,@"malformed controls are clamped, finite and limited to the supported schema");
NSMutableDictionary *off=[a.values mutableCopy];off[@(PGShapeEnabled)]=@0;off[@(PGColorEnabled)]=@0;off[@(PGSmoothingEnabled)]=@0;off[@(PGTrackBoxes)]=@1;
s=PGSettingsWithInspectorState(PGBeautyDefaults(),[PGInspectorState stateWithValues:off disabledParameters:[NSSet set] hasEdits:YES]);
Check(s.faceChin==0 && s.faceBigEye==0 && s.skinBrightness==0 && s.skinTonePreset==0 && s.midtoneDetail==0 && s.highPassDetail==0 && s.showTrackBox==0,@"card eye switches bypass the complete associated effect group");
}
static void ControllerTests(void) {
PGInspectorTestAPI *api=[PGInspectorTestAPI new];beatygrilPlugIn *plugin=[[beatygrilPlugIn alloc]initWithAPIManager:api];[plugin addParametersWithError:nil];
api.values[@(PGMidtone)]=@.5;
PGInspectorController *c=[[PGInspectorController alloc]initWithAPIManager:api];[c refresh];
Check(c.available && Near([c.state valueForParameter:PGMidtone],.5) && !c.state.hasEdits,@"initial UI reads the current legacy/template values instead of replacing them");
NSUInteger starts=api.actionStarts,writes=api.customWrites;
[c beginInteraction];[c setValue:.1 forParameter:PGChin];[c setValue:.3 forParameter:PGChin];[c setValue:.5 forParameter:PGChin];[c endInteraction];
Check(api.actionStarts==starts+1 && api.actionDepth==0 && api.customWrites==writes+3,@"a full continuous drag uses one balanced host action");
Check(api.scalarWrites==3,@"continuous drags mirror only changed values rather than rewriting every hidden parameter");
PGInspectorState *saved=api.values[@(PGInspectorPanel)];Check(saved.hasEdits && Near([saved valueForParameter:PGChin],.5) && Near([saved valueForParameter:PGMidtone],.5),@"published state contains the edited control and all untouched document values");
NSUInteger before=api.customWrites;[c setValue:.5 forParameter:PGChin];Check(api.customWrites==before,@"an unchanged value does not create duplicate undo writes");
PGInspectorController *restored=[[PGInspectorController alloc]initWithAPIManager:api];[restored refresh];Check([restored.state isEqual:saved],@"fresh inspector restores the full custom parameter state");
[c setValue:.37 forParameter:PGBrightness];[c toggleEnabledForParameter:PGBrightness];
Check(Near([c.state valueForParameter:PGBrightness],.37) && ![c.state isParameterEnabled:PGBrightness],@"eye bypass preserves the prior slider value");
[c toggleEnabledForParameter:PGBrightness];Check([c.state isParameterEnabled:PGBrightness] && Near([c.state valueForParameter:PGBrightness],.37),@"reenabling restores the exact amount");
[c toggleEnabledForParameter:PGBrightness];[c resetParameter:PGBrightness];Check([c.state isParameterEnabled:PGBrightness] && [c.state valueForParameter:PGBrightness]==0,@"reset restores default and clears that item's bypass");
NSDictionary *unchanged=[api.values copy];PGInspectorState *old=[c.state copy];api.rejectedWriteParameter=PGInspectorPanel;
Check(![c setValue:.9 forParameter:PGChin] && [api.values isEqual:unchanged] && [c.state isEqual:old] && c.statusMessage.length>0,@"rejected custom writes leave the UI and all render parameters unchanged");api.rejectedWriteParameter=0;
Check(![c setValue:NAN forParameter:PGChin] && ![c setValue:.3 forParameter:999] && ![c toggleEnabledForParameter:PGRadius],@"invalid edits and unsupported bypass are safely rejected");
[c setValue:-.4 forParameter:PGChin];NSDictionary *undo=api.undoValues;
api.values=[undo mutableCopy];[[NSRunLoop currentRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.36]];[c refresh];
Check(Near([c.state valueForParameter:PGChin],[old valueForParameter:PGChin]),@"host undo refresh restores the prior committed state after pending synchronization expires");
Check(api.accessViolations==0 && api.actionDepth==0 && api.actionStarts==api.actionEnds,@"all arbitrary-time reads and writes are inside balanced FxPlug action pairs");
[c setValue:.7 forParameter:PGBrightness];NSDictionary *beforeUndo=api.undoValues;
api.values=[beforeUndo mutableCopy];[c setValue:.2 forParameter:PGThin];
PGInspectorState *afterImmediateUndo=api.values[@(PGInspectorPanel)];
PGInspectorState *undoneState=beforeUndo[@(PGInspectorPanel)];
Check(Near([afterImmediateUndo valueForParameter:PGBrightness],[undoneState valueForParameter:PGBrightness]) && Near([afterImmediateUndo valueForParameter:PGThin],.2),@"a new gesture immediately after host undo cannot resurrect the undone value");
NSMutableDictionary *seekValues=[c.state.values mutableCopy];seekValues[@(PGChin)]=@(-.8);seekValues[@(PGMidtone)]=@.6;
api.values[@(PGInspectorPanel)]=[PGInspectorState stateWithValues:seekValues disabledParameters:[NSSet set] hasEdits:YES];api.hostTime=CMTimeAdd(api.hostTime,CMTimeMake(1,24));[c refresh];
Check(Near([c.state valueForParameter:PGChin],-.8) && Near([c.state valueForParameter:PGMidtone],.6),@"seeking invalidates pending display state immediately at the new CMTime");
[c setValue:.1 forParameter:PGBrightness];Check(Near([c.state valueForParameter:PGChin],-.8),@"new-time edits preserve the other controls from that time instead of a stale prior frame");
api.values=[api.undoValues mutableCopy];[c documentDidChange];
Check(Near([c.state valueForParameter:PGBrightness],[seekValues[@(PGBrightness)]doubleValue]),@"a host undo notification refreshes same-time state immediately without a timed wait");
PGHostAPI *legacy=[PGHostAPI new];beatygrilPlugIn *legacyPlugin=[[beatygrilPlugIn alloc]initWithAPIManager:legacy];[legacyPlugin addParametersWithError:nil];
legacy.values[@(PGDetailEnabled)]=@NO;legacy.values[@(PGHighPass)]=@.6;legacy.values[@(PGSharpness)]=@.4;
PGInspectorController *imported=[[PGInspectorController alloc]initWithAPIManager:legacy];[imported refresh];[imported setValue:.3 forParameter:PGBrightness];
PGBeautySettings migrated=PGSettingsWithInspectorState(PGBeautyDefaults(),imported.state);
Check(migrated.highPassDetail==0 && migrated.sharpness==0 && Near([imported.state valueForParameter:PGHighPass],.6),@"first panel edit preserves disabled legacy detail controls and their stored amounts");
legacy.values[@(PGInspectorPanel)]=[PGInspectorState stateWithValues:@{} disabledParameters:[NSSet set] hasEdits:NO];
legacy.values[@(PGDetailEnabled)]=@YES;legacy.values[@(PGSmoothingEnabled)]=@NO;
PGInspectorController *split=[[PGInspectorController alloc]initWithAPIManager:legacy];[split refresh];[split setValue:.2 forParameter:PGBrightness];
PGBeautySettings splitSettings=PGSettingsWithInspectorState(PGBeautyDefaults(),split.state);
Check(Near(splitSettings.highPassDetail,.6) && splitSettings.amount==0 && splitSettings.midtoneDetail==0,@"legacy global smoothing remains active when its old local-skin group was disabled");
Check(api.forwardRequests==0 && api.backwardRequests==0,@"normal inspector edits never request manual analysis");
[api.disabledAPIs addObject:@"FxCustomParameterActionAPI_v4"];
PGInspectorController *offline=[[PGInspectorController alloc]initWithAPIManager:api];[offline refresh];
Check(!offline.available && ![offline setValue:.2 forParameter:PGChin],@"missing host actions disable editing instead of unsafe parameter access");
}
static void ViewInteractionTests(void) {
PGHostAPI *api=[PGHostAPI new];beatygrilPlugIn *plugin=[[beatygrilPlugIn alloc]initWithAPIManager:api];[plugin addParametersWithError:nil];
PGInspectorView *view=(PGInspectorView *)[plugin createViewForParameterID:PGInspectorPanel];[view refreshFromHost];
NSSlider *chin=(NSSlider *)FindControl(view,NSSlider.class,@"下巴");Check(chin!=nil && chin.enabled,@"custom view exposes an accessible native chin slider");
chin.doubleValue=-.75;Fire(chin);PGInspectorState *s=api.values[@(PGInspectorPanel)];
Check(Near([s valueForParameter:PGChin],-.75) && [s valueForParameter:PGBigEye]==0,@"actual chin slider events do not write the eye parameter");
NSTextField *number=(NSTextField *)FindControl(view,NSTextField.class,@"下巴数值");number.stringValue=@"0.40";Fire(number);s=api.values[@(PGInspectorPanel)];Check(Near([s valueForParameter:PGChin],.4),@"editable numeric field writes its signed control");
number.stringValue=@"invalid";Fire(number);s=api.values[@(PGInspectorPanel)];Check(Near([s valueForParameter:PGChin],.4),@"invalid number entry preserves the last valid value");
NSMutableDictionary *changed=[s.values mutableCopy];changed[@(PGChin)]=@.8;
api.values[@(PGInspectorPanel)]=[PGInspectorState stateWithValues:changed disabledParameters:s.disabledParameters hasEdits:YES];
number.stringValue=@"0.40";Fire(number);s=api.values[@(PGInspectorPanel)];
Check(Near([s valueForParameter:PGChin],.4),@"explicit input matching old UI cache still overrides a newer host value");
NSButton *preview=(NSButton *)FindControl(view,NSButton.class,@"人脸框预览");preview.state=NSControlStateValueOn;Fire(preview);s=api.values[@(PGInspectorPanel)];Check([s valueForParameter:PGTrackBoxes]==1,@"preview switch events change the automatic face box setting");
for(NSNumber *width in @[@280,@320,@380,@520]) {
[view setFrameSize:NSMakeSize(width.doubleValue,20)];[view layoutSubtreeIfNeeded];
Check(view.frame.size.height>500 && Near(view.frame.size.height,view.fittingSize.height) && Near(view.frame.size.height,view.intrinsicContentSize.height),@"width changes produce a complete consistent intrinsic/fitting height");
for(NSView *card in view.subviews)if(!card.hidden)Check(NSMaxY(card.frame)<=NSHeight(view.bounds)+1 && NSMaxX(card.frame)<=NSWidth(view.bounds)+1,@"every visible card and footer remains inside the panel at all inspector widths");
}
}
int main(int argc,const char **argv) {@autoreleasepool {
[NSApplication sharedApplication];[NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
RegistrationTests();ModelTests();ControllerTests();ViewInteractionTests();
if(argc>2) {
NSData *inner=[NSData dataWithContentsOfFile:@(argv[1])],*outer=[NSData dataWithContentsOfFile:@(argv[2])];NSError *error=nil;
NSKeyedUnarchiver *reader=[[NSKeyedUnarchiver alloc]initForReadingFromData:inner error:&error];reader.requiresSecureCoding=YES;
PGInspectorState *state=[reader decodeObjectOfClass:PGInspectorState.class forKey:@"Custom Data"];[reader finishDecoding];
PGInspectorState *expected=[PGInspectorState stateWithValues:@{} disabledParameters:[NSSet set] hasEdits:NO];
Check(!error && !reader.error && [state isEqual:expected],@"the template's actual Custom Data bytes securely decode to our untouched state class");
NSKeyedUnarchiver *envelope=[[NSKeyedUnarchiver alloc]initForReadingFromData:outer error:&error];envelope.requiresSecureCoding=YES;
NSData *payload=[envelope decodeObjectOfClass:NSData.class forKey:@"BlindDataObject"];
NSNumber *legacy=[envelope decodeObjectOfClass:NSNumber.class forKey:@"DataIsLegacy"];[envelope finishDecoding];
Check(!error && !envelope.error && legacy && !legacy.boolValue && [payload isEqual:inner],@"Foundation reads the Motion envelope as the exact secure custom payload");
}
printf("Inspector: %lu checks, %lu failures\n",(unsigned long)Checks,(unsigned long)Failures);return Failures?1:0;
}}
+223
查看文件
@@ -0,0 +1,223 @@
#import <Foundation/Foundation.h>
#import <CoreImage/CoreImage.h>
#import <Metal/Metal.h>
#import "PGFaceProcessor.h"
#include <math.h>
@interface PGFaceProcessor (SlimTest)
- (CIImage *)warpImage:(CIImage *)image geometry:(NSArray<NSDictionary *> *)geometry settings:(PGBeautySettings)s extent:(CGRect)extent;
@end
@interface PGRealtimeFaceTracker : NSObject
- (NSDictionary *)resultForGeometry:(NSArray *)raw settings:(PGBeautySettings)s time:(CMTime)time extent:(CGRect)extent;
@end
static NSUInteger checks=0;
static double maxOracleError=0;
static void Check(BOOL ok,NSString *message) {checks++;if(!ok){fprintf(stderr,"FAIL: %s\n",message.UTF8String);exit(1);}}
static double Clamp(double x,double a,double b) {return fmin(b,fmax(a,x));}
static CGPoint ReadPoint(NSArray *a) {return CGPointMake([a[0] doubleValue],[a[1] doubleValue]);}
static NSArray *WritePoint(CGPoint p) {return @[@(p.x),@(p.y)];}
// Authored 2D projections, with the documented 76-point layout. They are not
// extracted vendor assets or private face coordinates. Positive yaw projects
// the nose toward the right while the silhouette becomes narrower.
static NSDictionary *Geometry(double yaw,double roll) {
NSMutableArray *p=[NSMutableArray new];
for(NSUInteger i=0;i<76;i++)[p addObject:@[@.5,@.5]];
p[6]=@[@.34,@.65];p[13]=@[@.66,@.65];
p[47]=@[@.5,@.60];p[48]=@[@.5,@.52];p[49]=@[@.5,@.45];p[52]=@[@.5,@.42];
p[36]=@[@.54,@.36];p[37]=@[@.5,@.35];p[38]=@[@.46,@.36];
for(NSUInteger i=0;i<17;i++) {
double t=M_PI+M_PI*i/16;
p[59+i]=@[@(.5+.29*cos(t)),@(.65+.43*sin(t))];
}
for(NSUInteger i=0;i<76;i++) {
CGPoint a=ReadPoint(p[i]);
double depth=(i==47 || i==48 || i==49 || i==52)?.18:.03;
double x=(a.x-.5)*cos(yaw)+depth*sin(yaw),y=a.y-.5;
p[i]=WritePoint(CGPointMake(.5+x*cos(roll)-y*sin(roll),.5+x*sin(roll)+y*cos(roll)));
}
NSMutableDictionary *g=[NSMutableDictionary dictionaryWithDictionary:@{@"box":@[@.15,@.15,@.7,@.6],@"points76":p}];
for(NSString *key in @[@"leftEye",@"rightEye",@"leftBrow",@"rightBrow",@"nose",@"lips",@"mouth",@"contour"])g[key]=@[];
return g;
}
// Independent scalar oracle from the recorded AIR specification. Its explicit
// y-down conversion checks the Core Image implementation's coordinate bridge.
static CGPoint Oracle(CGPoint pixel,NSDictionary *g,CGRect extent,double thin) {
static const unsigned int origins[17]={75,59,74,60,73,61,72,62,71,63,70,64,69,65,68,66,67};
static const unsigned int anchors[17]={6,13,47,47,47,47,48,48,48,48,49,49,49,49,52,52,47};
NSArray *raw=g[@"points76"];CGPoint p[76];
for(NSUInteger i=0;i<76;i++){p[i]=ReadPoint(raw[i]);p[i].y=1-p[i].y;}
double width=hypot((p[75].x-p[59].x)*extent.size.width,(p[75].y-p[59].y)*extent.size.height);
double height=hypot((p[67].x-(p[74].x+p[60].x)/2)*extent.size.width,(p[67].y-(p[74].y+p[60].y)/2)*extent.size.height);
double gain=Clamp(280/fmax(1,fmax(width,2*height)),1,2.25)*.025*Clamp(thin,0,1);
CGPoint q=CGPointMake((pixel.x-extent.origin.x)/extent.size.width,1-(pixel.y-extent.origin.y)/extent.size.height);
for(NSUInteger i=0;i<17;i++) {
CGPoint a=p[origins[i]],b=p[anchors[i]];
double r=hypot(a.x-b.x,a.y-b.y);
if(r<=.0001)continue;
double w=Clamp(1-hypot(q.x-a.x,q.y-a.y)/r,0,1);
q.x=Clamp(q.x-(b.x-a.x)*gain*w,0,1);q.y=Clamp(q.y-(b.y-a.y)*gain*w,0,1);
}
return CGPointMake(q.x,1-q.y);
}
static CGPoint ChinOracle(CGPoint pixel,NSDictionary *g,CGRect e,double thin,double chin) {
CGPoint q=Oracle(pixel,g,e,thin);q.y=1-q.y;
NSArray *raw=g[@"points76"];CGPoint points[76];
for(NSUInteger i=0;i<76;i++){points[i]=ReadPoint(raw[i]);points[i].y=1-points[i].y;}
double width=hypot((points[75].x-points[59].x)*e.size.width,(points[75].y-points[59].y)*e.size.height);
double height=2*hypot((points[67].x-(points[74].x+points[60].x)/2)*e.size.width,(points[67].y-(points[74].y+points[60].y)/2)*e.size.height);
double ratio=Clamp(chin,-1,1)*(chin>0?.04:.02)*Clamp(280/fmax(1,fmax(width,height)),1,2.25);
for(NSUInteger i=0;i<3;i++) {
CGPoint a=points[68-i],b=points[38-i];double r=hypot(a.x-b.x,a.y-b.y);
if(r<=.0001)continue;
double w=Clamp(1-hypot(q.x-a.x,q.y-a.y)/r,0,1);
q.x=Clamp(q.x-(b.x-a.x)*ratio*w,0,1);q.y=Clamp(q.y-(b.y-a.y)*ratio*w,0,1);
}
return CGPointMake(q.x,1-q.y);
}
static CIImage *CoordinateImage(CGRect extent) {
NSUInteger w=extent.size.width,h=extent.size.height;
NSMutableData *d=[NSMutableData dataWithLength:w*h*4*sizeof(float)];float *p=d.mutableBytes;
for(NSUInteger y=0;y<h;y++)for(NSUInteger x=0;x<w;x++) {
NSUInteger k=(y*w+x)*4;p[k]=(x+.5)/w;p[k+1]=(y+.5)/h;p[k+2]=.25;p[k+3]=1;
}
return [[CIImage imageWithBitmapData:d bytesPerRow:w*4*sizeof(float) size:extent.size format:kCIFormatRGBAf colorSpace:nil]
imageByApplyingTransform:CGAffineTransformMakeTranslation(extent.origin.x,extent.origin.y)];
}
static NSData *Pixels(CIContext *c,CIImage *image) {
CGRect e=image.extent;NSUInteger w=e.size.width,h=e.size.height;
NSMutableData *d=[NSMutableData dataWithLength:w*h*4*sizeof(float)];
[c render:image toBitmap:d.mutableBytes rowBytes:w*4*sizeof(float) bounds:e format:kCIFormatRGBAf colorSpace:nil];return d;
}
static NSData *Archive(NSDictionary *geometry) {
return [NSPropertyListSerialization dataWithPropertyList:@{@"version":@1,@"frames":@{@"0/1/0":@{@"value":@0,@"scale":@1,@"epoch":@0,@"faces":@[geometry]}}} format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
}
static void NumericSettingsCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDictionary *g,double amount,double chin,NSString *name) {
PGBeautySettings s=PGBeautyDefaults();s.faceThin=amount;s.faceChin=chin;
CIImage *input=CoordinateImage(e),*output=[p warpImage:input geometry:@[g] settings:s extent:e];
Check(output!=nil && CGRectEqualToRect(output.extent,e),[name stringByAppendingString:@" preserves extent"]);
NSData *data=Pixels(c,output);const float *pixels=data.bytes;double worst=0,maxMove=0;
NSUInteger worstX=0,worstY=0;CGPoint worstExpected=CGPointZero,worstActual=CGPointZero;
NSUInteger w=e.size.width,h=e.size.height;
for(NSUInteger y=3;y<h-3;y+=7)for(NSUInteger x=3;x<w-3;x+=7) {
// Bitmap rows are top first, while destCoord / geometry are y up.
CGPoint q=ChinOracle(CGPointMake(e.origin.x+x+.5,e.origin.y+h-y-.5),g,e,amount,chin);
double ex=Clamp(q.x,.5/w,1-.5/w),ey=Clamp(1-q.y,.5/h,1-.5/h);
NSUInteger k=(y*w+x)*4;
double difference=fmax(fabs(pixels[k]-ex),fabs(pixels[k+1]-ey));
if(difference>worst){worst=difference;worstX=x;worstY=y;worstExpected=CGPointMake(ex,ey);worstActual=CGPointMake(pixels[k],pixels[k+1]);}
maxMove=fmax(maxMove,hypot(ex-(x+.5)/w,ey-(y+.5)/h));
}
if(worst>0.00006)fprintf(stderr,"%s oracle max error=%g at %lu,%lu expected=(%g,%g) actual=(%g,%g)\n",name.UTF8String,worst,(unsigned long)worstX,(unsigned long)worstY,worstExpected.x,worstExpected.y,worstActual.x,worstActual.y);
maxOracleError=fmax(maxOracleError,worst);
Check(worst<0.00006,[name stringByAppendingString:@" GPU field matches the independent thin/chin oracle"]);
Check((amount==0 && chin==0) || maxMove>.0001,[name stringByAppendingString:@" exercises a nonzero displacement"]);
NSUInteger corner=4*((h-3)*w+(w-3));
Check(fabs(pixels[corner]-(w-2.5)/w)<1e-6 && fabs(pixels[corner+1]-(h-2.5)/h)<1e-6,[name stringByAppendingString:@" preserves distant background"]);
}
static void NumericCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDictionary *g,double amount,NSString *name) {
NumericSettingsCase(c,p,e,g,amount,0,name);
}
static CGPoint Displacement(CIContext *c,CIImage *ramp,CIImage *warped,CGPoint uv) {
CGRect e=ramp.extent;CGRect point=CGRectMake(floor(e.origin.x+uv.x*e.size.width),floor(e.origin.y+uv.y*e.size.height),1,1);
float a[4],b[4];
[c render:ramp toBitmap:a rowBytes:sizeof(a) bounds:point format:kCIFormatRGBAf colorSpace:nil];
[c render:warped toBitmap:b rowBytes:sizeof(b) bounds:point format:kCIFormatRGBAf colorSpace:nil];
return CGPointMake((b[0]-a[0])*e.size.width,-(b[1]-a[1])*e.size.height);
}
static void AnatomicalChecks(CIContext *c,PGFaceProcessor *p,NSDictionary *g,CGRect e) {
CIImage *ramp=CoordinateImage(e);NSArray *points=g[@"points76"];
CGPoint chin=ReadPoint(points[67]),lip=ReadPoint(points[37]);
double ax=(lip.x-chin.x)*e.size.width,ay=(lip.y-chin.y)*e.size.height;
for(int sign=-1;sign<=1;sign+=2) {
PGBeautySettings s=PGBeautyDefaults();s.faceChin=sign;
CIImage *out=[p warpImage:ramp geometry:@[g] settings:s extent:e];
CGPoint d=Displacement(c,ramp,out,chin);
Check(hypot(d.x,d.y)>.05,@"chin control moves the detected physical chin landmark");
Check(sign*(d.x*ax+d.y*ay)<0,@"signed chin direction follows chin-to-lip anchor instead of a fixed image Y axis");
BOOL outside=YES,eyesUnchanged=YES;
for(NSNumber *index in @[@6,@13]) {
CGPoint eye=ReadPoint(points[index.unsignedIntegerValue]);
for(NSUInteger i=0;i<3;i++) {
CGPoint a=ReadPoint(points[68-i]),b=ReadPoint(points[38-i]);
if(hypot(eye.x-a.x,eye.y-a.y)<=hypot(a.x-b.x,a.y-b.y)+.003)outside=NO;
}
CGPoint shift=Displacement(c,ramp,out,eye);eyesUnchanged &= hypot(shift.x,shift.y)<.002;
}
Check(outside && eyesUnchanged,@"both physical pupils outside chin supports remain unchanged");
}
}
int main(int argc,const char *argv[]) {@autoreleasepool {
CIContext *c=[CIContext contextWithMTLDevice:MTLCreateSystemDefaultDevice() options:@{kCIContextWorkingColorSpace:NSNull.null,kCIContextOutputColorSpace:NSNull.null}];
PGFaceProcessor *p=[PGFaceProcessor new];
CGRect extents[]={CGRectMake(0,0,240,240),CGRectMake(0,0,320,180),CGRectMake(-17,29,180,320),CGRectMake(0,0,640,360)};
double yaw[]={0,.65,1.40,-1.40},roll[]={0,.45,-.50};
NumericCase(c,p,extents[1],Geometry(0,0),0,@"zero strength");
for(NSUInteger e=0;e<4;e++)for(NSUInteger y=0;y<4;y++)for(NSUInteger r=0;r<3;r++) {
NSString *name=[NSString stringWithFormat:@"size%lu yaw%lu roll%lu",(unsigned long)e,(unsigned long)y,(unsigned long)r];
NumericCase(c,p,extents[e],Geometry(yaw[y],roll[r]),1,name);
}
NumericCase(c,p,extents[1],Geometry(.6,.2),.5,@"half strength");
for(NSUInteger e=0;e<3;e++)for(NSUInteger y=0;y<3;y++)for(NSUInteger r=0;r<2;r++)for(int sign=-1;sign<=1;sign+=2) {
double angle=y==0?0:(y==1?1.4:-1.4);
NumericSettingsCase(c,p,extents[e],Geometry(angle,r?.45:0),0,sign,
[NSString stringWithFormat:@"chin size%lu yaw%lu roll%lu sign%d",(unsigned long)e,(unsigned long)y,(unsigned long)r,sign]);
}
NumericSettingsCase(c,p,extents[1],Geometry(.4,.2),.8,1,@"thin plus positive chin");
NumericSettingsCase(c,p,extents[2],Geometry(-.6,-.2),1,-1,@"thin plus negative chin");
NumericSettingsCase(c,p,extents[0],Geometry(0,0),0,.5,@"half positive chin");
NumericSettingsCase(c,p,extents[0],Geometry(0,0),0,-.5,@"half negative chin");
CIImage *image=CoordinateImage(extents[1]);NSData *source=Pixels(c,image);
PGBeautySettings s=PGBeautyDefaults();s.faceThin=1;s.trackingEnabled=1;NSError *error=nil;
NSDictionary *g=Geometry(.9,.15);[p setTrackingData:Archive(g) forTarget:0];
PGFaceFrame *frame=[p processImage:image settings:s time:kCMTimeZero error:&error];
Check(frame!=nil && [frame.deformationGeometry[0][@"points76"] count]==76,@"offline restoration retains all 76 indexed points");
PGFaceProcessor *restored=[PGFaceProcessor new];[restored setTrackingData:[p trackingDataForTarget:0] forTarget:0];
Check([Pixels(c,[restored processImage:image settings:s time:kCMTimeZero error:&error].image) isEqualToData:Pixels(c,frame.image)],@"76-point geometry survives archive round trip with identical pixels");
NSData *imageWarp=Pixels(c,[p warpImage:frame.colorMask geometry:@[] settings:s extent:image.extent]);
Check(imageWarp.length==source.length,@"warped support masks retain dimensions");
// Apply the same coordinate field to an independent scalar mask ramp.
CIImage *mask=[image imageByApplyingFilter:@"CIColorMatrix" withInputParameters:@{@"inputRVector":[CIVector vectorWithX:1 Y:0 Z:0 W:0],@"inputGVector":[CIVector vectorWithX:1 Y:0 Z:0 W:0],@"inputBVector":[CIVector vectorWithX:1 Y:0 Z:0 W:0]}];
NSData *maskData=Pixels(c,[p warpImage:mask geometry:@[g] settings:s extent:image.extent]),*frameData=Pixels(c,frame.image);
const float *m=maskData.bytes,*v=frameData.bytes;
BOOL masksAgree=YES;for(NSUInteger i=0;i<source.length/sizeof(float);i+=4)if(fabs(m[i]-v[i])>1e-6){masksAgree=NO;break;}
Check(masksAgree,@"scalar support masks and RGB use the same source coordinates");
for(NSUInteger mode=0;mode<4;mode++) {
NSMutableDictionary *bad=[g mutableCopy];
if(mode==0)[bad removeObjectForKey:@"points76"];
if(mode==1)bad[@"points76"]=[g[@"points76"] subarrayWithRange:NSMakeRange(0,75)];
if(mode==2){NSMutableArray *a=[g[@"points76"] mutableCopy];a[65]=@[@(NAN),@.5];bad[@"points76"]=a;}
if(mode==3){NSMutableArray *a=[g[@"points76"] mutableCopy];a[65]=@[@9,@.5];bad[@"points76"]=a;}
[p setTrackingData:Archive(bad) forTarget:0];frame=[p processImage:image settings:s time:kCMTimeZero error:&error];
Check(frame!=nil && frame.deformationGeometry.count==1,@"legacy/malformed indexed points retain the valid face record");
Check([Pixels(c,frame.image) isEqualToData:source],@"legacy/malformed indexed points never trigger a guessed symmetric thin warp");
PGBeautySettings chinOnly=s;chinOnly.faceThin=0;chinOnly.faceChin=1;
Check([Pixels(c,[p processImage:image settings:chinOnly time:kCMTimeZero error:&error].image) isEqualToData:source],@"legacy/malformed indexed points never trigger guessed bbox chin deformation");
PGBeautySettings other=s;other.faceNarrow=.5;
Check(![Pixels(c,[p processImage:image settings:other time:kCMTimeZero error:&error].image) isEqualToData:source],@"other controls remain available in legacy archives");
}
PGRealtimeFaceTracker *tracker=[PGRealtimeFaceTracker new];PGBeautySettings live=PGBeautyDefaults();
NSDictionary *a=[tracker resultForGeometry:@[Geometry(0,0)] settings:live time:kCMTimeZero extent:image.extent];
NSDictionary *b=[tracker resultForGeometry:@[Geometry(.2,0)] settings:live time:CMTimeMake(1,25) extent:image.extent];
Check([a[@"output"][0][@"points76"] count]==76 && [b[@"output"][0][@"points76"] count]==76,@"realtime stabilization retains the complete indexed point layout");
Check([b isEqual:[tracker resultForGeometry:@[Geometry(.2,0)] settings:live time:CMTimeMake(1,25) extent:image.extent]],@"repeated rendering does not smooth the 76-point layout again");
NSDictionary *held=[tracker resultForGeometry:@[] settings:live time:CMTimeMake(2,25) extent:image.extent];
Check([held[@"output"][0][@"points76"] isEqual:b[@"output"][0][@"points76"]],@"missing detection holds the complete point layout");
NSDictionary *turned=Geometry(1.4,0);
NSDictionary *back=[tracker resultForGeometry:@[turned] settings:live time:CMTimeMake(3,25) extent:image.extent];
Check([back[@"output"][0][@"points76"] isEqual:turned[@"points76"]],@"reacquired side-face points do not blend with a held frontal pose");
for(int index=1;index<argc;index++) {@autoreleasepool {
CIImage *portrait=[CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:@(argv[index])] options:@{kCIImageColorSpace:NSNull.null}];
PGFaceProcessor *real=[PGFaceProcessor new];PGBeautySettings rs=PGBeautyDefaults();rs.faceThin=1;
PGFaceFrame *f=[real processImage:portrait settings:rs time:kCMTimeZero error:&error];
Check(f!=nil && f.faces.count>0 && [f.deformationGeometry[0][@"points76"] count]==76,@"real Vision detection supplies exactly 76 points to thin deformation");
Check(![Pixels(c,f.image) isEqualToData:Pixels(c,portrait)],@"maximum thin changes a real detected portrait");
Check([real analyzeImage:portrait settings:rs time:kCMTimeZero error:&error],@"real pose analysis saves the complete geometry");
PGFaceProcessor *copy=[PGFaceProcessor new];[copy setTrackingData:[real trackingDataForTarget:0] forTarget:0];rs.trackingEnabled=1;
Check([Pixels(c,[copy processImage:portrait settings:rs time:kCMTimeZero error:&error].image) isEqualToData:Pixels(c,f.image)],@"real pose replays from persisted 76 points with identical pixels");
AnatomicalChecks(c,real,f.deformationGeometry.firstObject,portrait.extent);
}}
printf("PASS: %lu slim alignment checks; maximum UV oracle error %.9g\n",(unsigned long)checks,maxOracleError);return 0;
}}