diff --git a/README.md b/README.md index 35c812c..8c7b59a 100644 --- a/README.md +++ b/README.md @@ -41,4 +41,4 @@ xcodebuild -project beatygril/beatygril.xcodeproj -scheme 'Wrapper Application' 使用AppKit自绘卡片、滑块和开关,原生数值输入、肤色圆点、单项/分组启停及重置。FCP仅发布自定义面板210和两项版权信息,撤掉平铺参数与手动分析板块。新面板使用独立NSSecureCoding状态对象完整保存设置,支持复制、撤销数据和数值插值;未编辑时保持旧文档参数值。旧分析数据不参与实时渲染。 -`./scripts/test-inspector.sh`验证参数路由、保存恢复、事务、绕过/重置和离屏布局;`python3 scripts/inspector-template.py audit Templates/大鱼人像美化/大鱼人像美化.moef`验证模板自定义数据和发布入口。离屏截图和模拟宿主检查不能替代Motion/FCP的实际嵌入、键盘/VoiceOver和工程重开验收,仍由用户自测。当前阶段尚未替换已安装1.0.6。 +`./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)。正式签名公证安装正在准备。 diff --git a/RECOVERY.md b/RECOVERY.md index 64ed64e..fc8644d 100644 --- a/RECOVERY.md +++ b/RECOVERY.md @@ -58,3 +58,13 @@ FCP模板从 `Templates/大鱼人像美化/` 恢复,将 `.moef` 和 `Drop Zone `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后替换,最终版本节点与回执随交付记录补齐。 + +兼容边界:未编辑面板时读取旧文档原参数;首次编辑会把当前数值采集为完整面板状态。旧版各项独立关键帧曲线尚未自动迁移为新面板关键帧,不宣称编辑前后任意旧动画逐帧等价。面板本身的数值插值与开关保持已做离屏验证,实际宿主关键帧交互仍待用户测试。 diff --git a/Templates/大鱼人像美化/VALIDATION.json b/Templates/大鱼人像美化/VALIDATION.json index b13903e..b6c0e71 100644 --- a/Templates/大鱼人像美化/VALIDATION.json +++ b/Templates/大鱼人像美化/VALIDATION.json @@ -153,5 +153,38 @@ "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" } } diff --git a/beatygril/beatygril/Plugin/PGInspectorState.h b/beatygril/beatygril/Plugin/PGInspectorState.h index 7ca08a2..25fab10 100644 --- a/beatygril/beatygril/Plugin/PGInspectorState.h +++ b/beatygril/beatygril/Plugin/PGInspectorState.h @@ -32,6 +32,7 @@ FOUNDATION_EXPORT PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings @property (nonatomic, copy, readonly) NSString *statusMessage; - (instancetype)initWithAPIManager:(id)manager; - (void)refresh; +- (void)documentDidChange; - (void)beginInteraction; - (void)endInteraction; - (BOOL)setValue:(double)value forParameter:(UInt32)identifier; diff --git a/beatygril/beatygril/Plugin/PGInspectorState.m b/beatygril/beatygril/Plugin/PGInspectorState.m index 5acc7ca..edb54d5 100644 --- a/beatygril/beatygril/Plugin/PGInspectorState.m +++ b/beatygril/beatygril/Plugin/PGInspectorState.m @@ -116,7 +116,7 @@ PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorStat } 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; + 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; } @@ -128,6 +128,7 @@ PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorStat @property (nonatomic,strong) id action; @property (nonatomic) BOOL interacting; @property (nonatomic) NSTimeInterval pendingUntil; +@property (nonatomic) CMTime pendingTime; @end @implementation PGInspectorController - (instancetype)initWithAPIManager:(id)manager { @@ -145,6 +146,7 @@ PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorStat } - (void)finish {if(_interacting){_interacting=NO;[_action endAction:self];_action=nil;}} - (void)readAtTime:(CMTime)time { + if(_pendingUntil && CMTimeCompare(time,_pendingTime)!=0)_pendingUntil=0; id api=[_manager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]; NSObject *object=nil;[api getCustomParameterValue:&object fromParameter:PGInspectorPanel atTime:time]; if([object isKindOfClass:PGInspectorState.class] && [(PGInspectorState *)object hasEdits]) { @@ -162,16 +164,31 @@ PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorStat else [api getFloatValue:&value fromParameter:key atTime:time]; values[@(key)]=@(value); } - _state=[PGInspectorState stateWithValues:values disabledParameters:[NSSet set] hasEdits:NO]; + 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])[self readAtTime:[_action currentTime]]; + 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 { @@ -190,7 +207,7 @@ PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorStat 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;_statusMessage=@"";return YES; + _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; @@ -202,7 +219,11 @@ PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorStat } - (BOOL)toggleEnabledForParameter:(UInt32)identifier { NSDictionary *d=PGInspectorDescriptor(identifier);if(!d)return NO; - if([d[@"kind"] isEqual:@"master"] || [d[@"kind"] isEqual:@"toggle"])return [self setValue:![self.state valueForParameter:identifier] forParameter:identifier]; + 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 { diff --git a/beatygril/beatygril/Plugin/PGInspectorView.h b/beatygril/beatygril/Plugin/PGInspectorView.h index 23bc831..90ca9bc 100644 --- a/beatygril/beatygril/Plugin/PGInspectorView.h +++ b/beatygril/beatygril/Plugin/PGInspectorView.h @@ -10,6 +10,7 @@ NS_ASSUME_NONNULL_BEGIN /// Also permits a deterministic controller in an offscreen preview or test. - (instancetype)initWithController:(PGInspectorController *)controller; - (void)refreshFromHost; +- (void)hostParameterChanged; @end NS_ASSUME_NONNULL_END diff --git a/beatygril/beatygril/Plugin/PGInspectorView.m b/beatygril/beatygril/Plugin/PGInspectorView.m index f511000..a11542a 100644 --- a/beatygril/beatygril/Plugin/PGInspectorView.m +++ b/beatygril/beatygril/Plugin/PGInspectorView.m @@ -253,7 +253,10 @@ static PGInspectorGlyphButton *PGUIButton(NSString *glyph, NSColor *accent, id t } if(number&&isfinite(number.doubleValue)){ double value=number.doubleValue/PGValueScale(_descriptor);value=fmin([_descriptor[@"max"]doubleValue],fmax([_descriptor[@"min"]doubleValue],value)); - if(fabs(value-[_inspector.controller.state valueForParameter:[_descriptor[@"id"]unsignedIntValue]])>.0000001)[_inspector writeValue:value parameter:[_descriptor[@"id"]unsignedIntValue]]; + // 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]]); } @@ -331,6 +334,7 @@ static PGInspectorGlyphButton *PGUIButton(NSString *glyph, NSColor *accent, id t } - (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];} } diff --git a/beatygril/beatygril/Plugin/beatygrilPlugIn.m b/beatygril/beatygril/Plugin/beatygrilPlugIn.m index 946859d..97470c0 100644 --- a/beatygril/beatygril/Plugin/beatygrilPlugIn.m +++ b/beatygril/beatygril/Plugin/beatygrilPlugIn.m @@ -93,6 +93,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) { @property BOOL analysisForward; @property (strong) NSArray *loadedTracks; @property BOOL reportedRender; +@property (strong) NSHashTable *inspectorViews; @end @implementation beatygrilPlugIn @@ -103,6 +104,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) { _analysisFaceProcessor=[PGFaceProcessor new]; _contexts=[NSMutableDictionary new]; _analysisSettings=PGBeautyDefaults(); _analysisTime=kCMTimeZero; + _inspectorViews=[NSHashTable weakObjectsHashTable]; } return self; } @@ -165,7 +167,16 @@ static CIImage *PGImageFromTile(FxImageTile *tile) { return ok ? YES : PGFail(error,@"宿主未能创建完整的人像美化参数。"); } - (NSView *)createViewForParameterID:(UInt32)identifier { - return identifier==PGInspectorPanel ? [[PGInspectorView alloc] initWithAPIManager:_apiManager] : nil; + 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 *)classesForCustomParameterID:(UInt32)identifier { diff --git a/docs/verification/v1.1.0/core-tests.txt b/docs/verification/v1.1.0/core-tests.txt new file mode 100644 index 0000000..e46c2c6 --- /dev/null +++ b/docs/verification/v1.1.0/core-tests.txt @@ -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 diff --git a/docs/verification/v1.1.0/host-tests.txt b/docs/verification/v1.1.0/host-tests.txt new file mode 100644 index 0000000..b01837a --- /dev/null +++ b/docs/verification/v1.1.0/host-tests.txt @@ -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 diff --git a/docs/verification/v1.1.0/inspector-preview.png b/docs/verification/v1.1.0/inspector-preview.png new file mode 100644 index 0000000..422a58c Binary files /dev/null and b/docs/verification/v1.1.0/inspector-preview.png differ diff --git a/docs/verification/v1.1.0/inspector-tests.txt b/docs/verification/v1.1.0/inspector-tests.txt new file mode 100644 index 0000000..abeccc2 --- /dev/null +++ b/docs/verification/v1.1.0/inspector-tests.txt @@ -0,0 +1 @@ +Inspector: 134 checks, 0 failures diff --git a/docs/verification/v1.1.0/osc-tests.txt b/docs/verification/v1.1.0/osc-tests.txt new file mode 100644 index 0000000..b213862 --- /dev/null +++ b/docs/verification/v1.1.0/osc-tests.txt @@ -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 diff --git a/docs/verification/v1.1.0/template-audit.json b/docs/verification/v1.1.0/template-audit.json new file mode 100644 index 0000000..43160e0 --- /dev/null +++ b/docs/verification/v1.1.0/template-audit.json @@ -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" +} diff --git a/docs/verification/v1.1.0/ui-validation.json b/docs/verification/v1.1.0/ui-validation.json new file mode 100644 index 0000000..4a68b34 --- /dev/null +++ b/docs/verification/v1.1.0/ui-validation.json @@ -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" +} diff --git a/docs/验证记录.md b/docs/验证记录.md index a5a5fad..c1136f2 100644 --- a/docs/验证记录.md +++ b/docs/验证记录.md @@ -81,3 +81,11 @@ Motion:独立普通视频项目,源PTS0、1920×1080/25fps/10秒,媒体通 配套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及工程重开由用户自测。源码与回执会按正式交付节点保存。 diff --git a/scripts/test-inspector.sh b/scripts/test-inspector.sh index e891d28..b439277 100755 --- a/scripts/test-inspector.sh +++ b/scripts/test-inspector.sh @@ -8,6 +8,8 @@ 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 \ @@ -19,4 +21,4 @@ xcrun clang "${compiler_extra[@]}" -fobjc-arc -fmodules -Wall -Wextra -Wno-unuse -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/inspector-tests" "$output_dir/template-inner.bplist" "$output_dir/template-outer.bplist" diff --git a/tests/host_adapter_tests.m b/tests/host_adapter_tests.m index b9e9449..c85ad80 100644 --- a/tests/host_adapter_tests.m +++ b/tests/host_adapter_tests.m @@ -409,7 +409,7 @@ static void TrackingCoordinateTests(NSDictionary *archive, NSDictionary *hostVal 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 restores a marked coordinate archive"); + 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)); @@ -545,7 +545,7 @@ int main(int argc, const char *argv[]) { api.values[@(PGSelection)] = @[@0.43, @0.76]; NSData *oldRenderState = Snapshot(plugin, api.hostTime); [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)); [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"); @@ -569,7 +569,7 @@ int main(int argc, const char *argv[]) { 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); 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]; Check(api.backwardRequests == 1, @"backward analyze button calls host API"); [plugin desiredAnalysisTimeRange:&desired forInputWithTimeRange:clip error:&error]; diff --git a/tests/inspector_tests.m b/tests/inspector_tests.m index eab39a2..f9161e9 100644 --- a/tests/inspector_tests.m +++ b/tests/inspector_tests.m @@ -71,9 +71,9 @@ static void ModelTests(void) { 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; + 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,@"card eye switches bypass the complete associated effect group"); + 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]; @@ -98,6 +98,27 @@ static void ControllerTests(void) { 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]; @@ -111,6 +132,10 @@ static void ViewInteractionTests(void) { 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]; @@ -118,8 +143,19 @@ static void ViewInteractionTests(void) { 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(void) {@autoreleasepool { +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; }}