@pilio/gemini-watermark-remover 1.0.31 → 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/video-app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @pilio/gemini-watermark-remover v1.0.31+bf77e39
2
+ * @pilio/gemini-watermark-remover v1.0.32+6fab72e
3
3
  * Automatically removes watermarks from Gemini AI generated images
4
4
  * (c) 2026 GargantuaX
5
5
  * https://github.com/GargantuaX/gemini-watermark-remover
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pilio/gemini-watermark-remover",
3
3
  "description": "Automatically removes watermarks from Gemini AI generated images",
4
- "version": "1.0.31",
4
+ "version": "1.0.32",
5
5
  "author": "GargantuaX",
6
6
  "sideEffects": false,
7
7
  "files": [
@@ -196,6 +196,52 @@ export function computeSizeAdjustedConfidence(confidence, size, referenceSize =
196
196
  return confidence * sizeWeight;
197
197
  }
198
198
 
199
+ function compareAdaptiveCandidatePriority(left, right) {
200
+ const leftScore = Number.isFinite(left?.adjustedScore)
201
+ ? left.adjustedScore
202
+ : computeSizeAdjustedConfidence(left?.confidence, left?.size);
203
+ const rightScore = Number.isFinite(right?.adjustedScore)
204
+ ? right.adjustedScore
205
+ : computeSizeAdjustedConfidence(right?.confidence, right?.size);
206
+ if (Math.abs(leftScore - rightScore) > 1e-12) {
207
+ return leftScore - rightScore;
208
+ }
209
+
210
+ const leftSize = Number.isFinite(left?.size) ? left.size : 0;
211
+ const rightSize = Number.isFinite(right?.size) ? right.size : 0;
212
+ const leftDistance = Math.abs(leftSize - REFERENCE_WATERMARK_SIZE);
213
+ const rightDistance = Math.abs(rightSize - REFERENCE_WATERMARK_SIZE);
214
+ if (leftDistance !== rightDistance) {
215
+ return rightDistance - leftDistance;
216
+ }
217
+
218
+ const leftConfidence = Number.isFinite(left?.confidence) ? left.confidence : 0;
219
+ const rightConfidence = Number.isFinite(right?.confidence) ? right.confidence : 0;
220
+ if (leftConfidence !== rightConfidence) {
221
+ return leftConfidence - rightConfidence;
222
+ }
223
+ if (leftSize !== rightSize) {
224
+ return leftSize - rightSize;
225
+ }
226
+
227
+ const leftX = Number.isFinite(left?.x) ? left.x : Infinity;
228
+ const rightX = Number.isFinite(right?.x) ? right.x : Infinity;
229
+ if (leftX !== rightX) {
230
+ return rightX - leftX;
231
+ }
232
+ const leftY = Number.isFinite(left?.y) ? left.y : Infinity;
233
+ const rightY = Number.isFinite(right?.y) ? right.y : Infinity;
234
+ return rightY - leftY;
235
+ }
236
+
237
+ export function selectAdaptiveFineCandidate(currentBest, candidate) {
238
+ if (!currentBest) return candidate;
239
+ if (!candidate) return currentBest;
240
+ return compareAdaptiveCandidatePriority(candidate, currentBest) > 0
241
+ ? candidate
242
+ : currentBest;
243
+ }
244
+
199
245
  function buildSeedConfigs(width, height, defaultConfig) {
200
246
  // Start adaptive search from both the coarse default anchor and any
201
247
  // catalog-projected anchors for official or near-official Gemini sizes.
@@ -445,10 +491,10 @@ export function detectAdaptiveWatermarkRegion({
445
491
  })
446
492
  .filter(Boolean);
447
493
 
448
- const bestSeed = seedCandidates.reduce((best, candidate) => {
449
- if (!best || candidate.confidence > best.confidence) return candidate;
450
- return best;
451
- }, null);
494
+ const bestSeed = seedCandidates.reduce(
495
+ (best, candidate) => selectAdaptiveFineCandidate(best, candidate),
496
+ null
497
+ );
452
498
  if (bestSeed && bestSeed.confidence >= threshold + 0.08) {
453
499
  return {
454
500
  found: true,
@@ -483,7 +529,7 @@ export function detectAdaptiveWatermarkRegion({
483
529
  const topK = [];
484
530
  const pushTopK = (candidate) => {
485
531
  topK.push(candidate);
486
- topK.sort((a, b) => b.adjustedScore - a.adjustedScore);
532
+ topK.sort((left, right) => -compareAdaptiveCandidatePriority(left, right));
487
533
  if (topK.length > 5) topK.length = 5;
488
534
  };
489
535
 
@@ -545,14 +591,12 @@ export function detectAdaptiveWatermarkRegion({
545
591
  const score = scoreCandidate(context, tpl.alpha, tpl.grad, { x, y, size });
546
592
  if (!score) continue;
547
593
 
548
- if (score.confidence > best.confidence) {
549
- best = {
550
- x,
551
- y,
552
- size,
553
- ...score
554
- };
555
- }
594
+ best = selectAdaptiveFineCandidate(best, {
595
+ x,
596
+ y,
597
+ size,
598
+ ...score
599
+ });
556
600
  }
557
601
  }
558
602
  }
@@ -137,6 +137,8 @@ const BOTTOM_RIGHT_48_EVIDENCE_DOMINANCE_GRADIENT_ADVANTAGE = 0.12;
137
137
  const BOTTOM_RIGHT_48_EVIDENCE_DOMINANCE_MAX_RESIDUAL_DELTA = 0.08;
138
138
  const DARK_POLARITY_CATALOG_MIN_ORIGINAL_SPATIAL = 0.12;
139
139
  const DARK_POLARITY_CATALOG_MIN_ORIGINAL_GRADIENT = 0.08;
140
+ const DARK_POLARITY_48_MIN_ORIGINAL_SPATIAL = 0.6;
141
+ const DARK_POLARITY_48_MIN_ORIGINAL_GRADIENT = 0.3;
140
142
  const DARK_POLARITY_CATALOG_MAX_TEXTURE_FOR_WEAK_EVIDENCE = 0.25;
141
143
  const DARK_POLARITY_MAX_NEAR_WHITE_RATIO_INCREASE_FOR_WEAK_EVIDENCE = 0.05;
142
144
  const DARK_POLARITY_NEAR_WHITE_OVERRIDE_MIN_ORIGINAL_SPATIAL = 0.4;
@@ -183,6 +185,7 @@ const PREVIEW_ANCHOR_GAIN_SKIP_RESIDUAL_THRESHOLD = 0.24;
183
185
  const PREVIEW_ANCHOR_GAIN_SKIP_GRADIENT_THRESHOLD = 0.24;
184
186
  const CORE_ALPHA_PRIORITY_GAINS = Object.freeze([0.6, 1, 1.1, 1.15, 1.3, 0.45, 0.7, 0.85, 0.55]);
185
187
  const CURRENT_LARGE_MARGIN_ULTRA_WEAK_ALPHA_GAINS = Object.freeze([0.25, 0.3, 0.35, 0.4]);
188
+ const DARK_POLARITY_48_ULTRA_WEAK_ALPHA_GAINS = Object.freeze([0.08, 0.1, 0.12, 0.15, 0.2]);
186
189
  const STANDARD_ANCHOR_WEAK_ALPHA_RESCUE_GAINS = Object.freeze([0.55, 0.7, 0.85]);
187
190
  const STANDARD_ANCHOR_WEAK_RESCUE_MAX_SPATIAL = 0.35;
188
191
  const STANDARD_ANCHOR_WEAK_RESCUE_MAX_GRADIENT = 0.24;
@@ -393,13 +396,17 @@ function buildStandardCandidateSeeds({
393
396
  }
394
397
 
395
398
  if (shouldAddDarkPolaritySeed(candidateConfig)) {
399
+ const weakDarkPolarity48 = candidateConfig.logoSize === 48;
396
400
  seeds.push({
397
401
  ...baseSeed,
398
402
  alphaMap: createNegativeAlphaMap(alphaMap),
399
403
  source: `${baseSeed.source}+dark-polarity`,
400
404
  provenance: mergeCandidateProvenance(
401
405
  baseSeed.provenance,
402
- { darkPolarity: true }
406
+ {
407
+ darkPolarity: true,
408
+ ...(weakDarkPolarity48 ? { weakDarkPolarity48: true } : {})
409
+ }
403
410
  )
404
411
  });
405
412
  }
@@ -409,9 +416,19 @@ function buildStandardCandidateSeeds({
409
416
  }
410
417
 
411
418
  function shouldAddDarkPolaritySeed(config) {
412
- return config?.logoSize === 96 &&
419
+ if (
420
+ config?.logoSize === 96 &&
413
421
  config.marginRight === 192 &&
414
- config.marginBottom === 192;
422
+ config.marginBottom === 192
423
+ ) {
424
+ return true;
425
+ }
426
+
427
+ return config?.logoSize === 48 &&
428
+ config.marginRight >= 90 &&
429
+ config.marginRight <= 100 &&
430
+ config.marginBottom >= 90 &&
431
+ config.marginBottom <= 100;
415
432
  }
416
433
 
417
434
  const negativeAlphaMapCache = new WeakMap();
@@ -659,7 +676,15 @@ function isWeakAlphaPrioritySeed(seed) {
659
676
  seed.config.marginBottom === 96;
660
677
  }
661
678
 
679
+ function isWeakDarkPolarity48Seed(seed) {
680
+ return seed?.provenance?.weakDarkPolarity48 === true;
681
+ }
682
+
662
683
  function resolveStandardSeedAlphaPriorityGains(seed, alphaPriorityGains) {
684
+ if (isWeakDarkPolarity48Seed(seed)) {
685
+ return DARK_POLARITY_48_ULTRA_WEAK_ALPHA_GAINS;
686
+ }
687
+
663
688
  const extras = isWeakAlphaPrioritySeed(seed)
664
689
  ? [
665
690
  ...CURRENT_LARGE_MARGIN_ULTRA_WEAK_ALPHA_GAINS,
@@ -874,7 +899,7 @@ function evaluateStandardTrialForSeed({
874
899
  }
875
900
  }
876
901
 
877
- return isWeakAlphaPrioritySeed(seed)
902
+ const selected = isWeakAlphaPrioritySeed(seed)
878
903
  ? bestWeakAlphaPriorityTrial ?? bestWeakAlphaRescueTrial ?? bestAcceptedTrial ?? fallbackTrial
879
904
  : bestWeakAlphaPriorityTrial ??
880
905
  (
@@ -884,6 +909,10 @@ function evaluateStandardTrialForSeed({
884
909
  ) ??
885
910
  bestWeakAlphaRescueTrial ??
886
911
  fallbackTrial;
912
+
913
+ return isWeakDarkPolarity48Seed(seed) && selected?.accepted !== true
914
+ ? null
915
+ : selected;
887
916
  }
888
917
 
889
918
  export function evaluateRestorationCandidate({
@@ -1011,11 +1040,40 @@ export function evaluateRestorationCandidate({
1011
1040
  const strongDarkPolarityOriginalEvidence =
1012
1041
  originalScores.spatialScore >= DARK_POLARITY_CATALOG_MIN_ORIGINAL_SPATIAL ||
1013
1042
  originalScores.gradientScore >= DARK_POLARITY_CATALOG_MIN_ORIGINAL_GRADIENT;
1043
+ const isLargeMargin48DarkPolarity =
1044
+ provenance?.darkPolarity === true &&
1045
+ config?.logoSize === 48 &&
1046
+ config.marginRight >= 90 &&
1047
+ config.marginRight <= 100 &&
1048
+ config.marginBottom >= 90 &&
1049
+ config.marginBottom <= 100;
1050
+ const largeMargin48DarkPolarityEvidenceAllowed =
1051
+ !isLargeMargin48DarkPolarity ||
1052
+ (
1053
+ originalScores.spatialScore >= DARK_POLARITY_48_MIN_ORIGINAL_SPATIAL &&
1054
+ originalScores.gradientScore >= DARK_POLARITY_48_MIN_ORIGINAL_GRADIENT
1055
+ );
1056
+ const weakDarkPolarity48ScopeAllowed =
1057
+ provenance?.weakDarkPolarity48 !== true ||
1058
+ (
1059
+ config?.logoSize === 48 &&
1060
+ config.marginRight >= 90 &&
1061
+ config.marginRight <= 100 &&
1062
+ config.marginBottom >= 90 &&
1063
+ config.marginBottom <= 100 &&
1064
+ alphaGain <= 0.2
1065
+ );
1014
1066
  const darkPolarityCatalogEvidenceAllowed =
1015
1067
  provenance?.darkPolarity !== true ||
1016
1068
  provenance?.catalogVariant !== true ||
1017
- strongDarkPolarityOriginalEvidence ||
1018
- texturePenalty <= DARK_POLARITY_CATALOG_MAX_TEXTURE_FOR_WEAK_EVIDENCE;
1069
+ (
1070
+ weakDarkPolarity48ScopeAllowed &&
1071
+ largeMargin48DarkPolarityEvidenceAllowed &&
1072
+ (
1073
+ strongDarkPolarityOriginalEvidence ||
1074
+ texturePenalty <= DARK_POLARITY_CATALOG_MAX_TEXTURE_FOR_WEAK_EVIDENCE
1075
+ )
1076
+ );
1019
1077
  const strongDarkPolarityNearWhiteOverrideEvidence =
1020
1078
  originalScores.spatialScore >= DARK_POLARITY_NEAR_WHITE_OVERRIDE_MIN_ORIGINAL_SPATIAL ||
1021
1079
  originalScores.gradientScore >= DARK_POLARITY_NEAR_WHITE_OVERRIDE_MIN_ORIGINAL_GRADIENT;
@@ -465,7 +465,6 @@ export function resolveOfficialGeminiSearchConfigEntries(
465
465
  })
466
466
  .filter(Boolean)
467
467
  .sort((a, b) => a.score - b.score);
468
-
469
468
  const deduped = [];
470
469
  const seen = new Set();
471
470
  for (const candidate of candidates) {
@@ -27,6 +27,14 @@ function createSelectedCandidate(best) {
27
27
  };
28
28
  }
29
29
 
30
+ function findDarkBackgroundSupportConvergence(meta = {}) {
31
+ const stages = Array.isArray(meta.alphaAdjustmentStages)
32
+ ? meta.alphaAdjustmentStages
33
+ : [];
34
+ const convergence = stages.at(-1)?.darkBackgroundSupportConvergence;
35
+ return convergence?.accepted === true ? convergence : null;
36
+ }
37
+
30
38
  function createRuntimeFailureResult({
31
39
  createRejectedResult,
32
40
  originalImageData,
@@ -42,6 +50,26 @@ function createRuntimeFailureResult({
42
50
  });
43
51
  }
44
52
 
53
+ function createNoWatermarkResult({
54
+ createRejectedResult,
55
+ originalImageData,
56
+ collection,
57
+ debugTimings
58
+ }) {
59
+ const selection = collection?.automaticSelection ?? collection?.fixedSelection ?? {};
60
+ return createRejectedResult({
61
+ imageData: originalImageData,
62
+ debugTimings,
63
+ reason: 'no-watermark-detected',
64
+ adaptiveConfidence: selection.adaptiveConfidence ?? null,
65
+ originalSpatialScore: selection.standardSpatialScore ?? null,
66
+ originalGradientScore: selection.standardGradientScore ?? null,
67
+ source: 'skipped',
68
+ decisionTier: selection.decisionTier ?? 'insufficient',
69
+ selectionDebug: null
70
+ });
71
+ }
72
+
45
73
  function notifyCandidateCompleted({ options, candidate, debugTimings }) {
46
74
  if (typeof options?.onCandidateCompleted !== 'function') return;
47
75
 
@@ -55,6 +83,41 @@ function notifyCandidateCompleted({ options, candidate, debugTimings }) {
55
83
  }
56
84
  }
57
85
 
86
+ function appendUniqueRiskFlag(flags, flag) {
87
+ const normalized = Array.isArray(flags) ? flags : [];
88
+ return normalized.includes(flag) ? normalized : [...normalized, flag];
89
+ }
90
+
91
+ function attachPresenceBestEffortMeta(meta, collection) {
92
+ if (collection?.bestEffortFallback !== true) return meta;
93
+
94
+ const riskFlag = 'unconfirmed-watermark-presence';
95
+ const decisionPath = meta?.decisionPath && typeof meta.decisionPath === 'object'
96
+ ? {
97
+ ...meta.decisionPath,
98
+ riskFlags: appendUniqueRiskFlag(meta.decisionPath.riskFlags, riskFlag),
99
+ evaluation: meta.decisionPath.evaluation &&
100
+ typeof meta.decisionPath.evaluation === 'object'
101
+ ? {
102
+ ...meta.decisionPath.evaluation,
103
+ riskFlags: appendUniqueRiskFlag(
104
+ meta.decisionPath.evaluation.riskFlags,
105
+ riskFlag
106
+ )
107
+ }
108
+ : meta.decisionPath.evaluation
109
+ }
110
+ : meta?.decisionPath;
111
+
112
+ return {
113
+ ...meta,
114
+ presenceConfirmed: false,
115
+ bestEffortReason:
116
+ collection.bestEffortReason ?? 'presence-witness-unconfirmed',
117
+ decisionPath
118
+ };
119
+ }
120
+
58
121
  export function runImageWatermarkPipeline({
59
122
  imageData,
60
123
  options = {},
@@ -119,6 +182,26 @@ export function runImageWatermarkPipeline({
119
182
  debugTimings.generatedCandidateCount = hypotheses.length;
120
183
  debugTimings.earlyExitReason = null;
121
184
  }
185
+ if (
186
+ collection?.presenceConfirmed === false &&
187
+ collection?.bestEffortFallback !== true
188
+ ) {
189
+ if (debugTimingsEnabled) {
190
+ debugTimings.candidateExecutionMs = 0;
191
+ debugTimings.executedCandidateCount = 0;
192
+ debugTimings.completedCandidateCount = 0;
193
+ debugTimings.failedCandidateCount = 0;
194
+ debugTimings.candidateRankingMs = 0;
195
+ debugTimings.earlyExitReason = 'no-watermark-detected';
196
+ debugTimings.totalMs = nowMs() - totalStartedAt;
197
+ }
198
+ return createNoWatermarkResult({
199
+ createRejectedResult,
200
+ originalImageData,
201
+ collection,
202
+ debugTimings
203
+ });
204
+ }
122
205
 
123
206
  const completed = [];
124
207
  const failures = [];
@@ -135,7 +218,9 @@ export function runImageWatermarkPipeline({
135
218
  debugTimingsEnabled,
136
219
  visualPostProcessingEnabled,
137
220
  cleanupConfig,
138
- createAcceptedPipelineDependencies,
221
+ createAcceptedPipelineDependencies: () => (
222
+ createAcceptedPipelineDependencies(hypothesis)
223
+ ),
139
224
  runAcceptedPipeline,
140
225
  createAcceptedFinalResult
141
226
  });
@@ -148,7 +233,16 @@ export function runImageWatermarkPipeline({
148
233
  qualitySignals: measureCandidate({
149
234
  originalImageData,
150
235
  candidateImageData: completedCandidate.result.imageData,
151
- hypothesis
236
+ hypothesis,
237
+ finalCandidate: {
238
+ position: completedCandidate.pipelineState?.position,
239
+ alphaMap: completedCandidate.pipelineState?.alphaMap,
240
+ alphaGain: completedCandidate.pipelineState?.alphaGain,
241
+ darkBackgroundSupportConvergence:
242
+ findDarkBackgroundSupportConvergence(
243
+ completedCandidate.result.meta
244
+ )
245
+ }
152
246
  })
153
247
  };
154
248
  completed.push(candidate);
@@ -197,13 +291,14 @@ export function runImageWatermarkPipeline({
197
291
  }
198
292
 
199
293
  const candidateSummaries = createSummaries(ranked, failures);
200
- const meta = attachSelectionMeta(best.result.meta, {
294
+ const selectionMeta = attachSelectionMeta(best.result.meta, {
201
295
  qualityStatus: best.qualitySignals?.qualityStatus,
202
296
  selectionConfidence: best.selectionConfidence,
203
297
  selectedCandidate: createSelectedCandidate(best),
204
298
  qualitySignals: best.qualitySignals,
205
299
  candidateSummaries
206
300
  });
301
+ const meta = attachPresenceBestEffortMeta(selectionMeta, collection);
207
302
  if (debugTimingsEnabled) {
208
303
  debugTimings.candidateRankingMs = nowMs() - rankingStartedAt;
209
304
  debugTimings.totalMs = nowMs() - totalStartedAt;
@@ -96,11 +96,15 @@ export function runAcceptedAlphaRepairPipeline({
96
96
  originalGradientScore: current.originalGradientScore,
97
97
  calculateNearBlackRatio: metrics.calculateNearBlackRatio,
98
98
  acceptCurrentAlphaTrialResult,
99
- debugTimings,
100
- debugTimingsEnabled,
101
- refiners: {
102
- recalibrateOverSubtractedAlpha: refiners.recalibrateOverSubtractedAlpha,
103
- fineTuneDarkCatalogAlpha: refiners.fineTuneDarkCatalogAlpha,
99
+ debugTimings,
100
+ debugTimingsEnabled,
101
+ refiners: {
102
+ refineLargeMargin48ProfileAlphaRescue:
103
+ refiners.refineLargeMargin48ProfileAlphaRescue,
104
+ fineTuneEvidenceGatedLocalAlpha:
105
+ refiners.fineTuneEvidenceGatedLocalAlpha,
106
+ recalibrateOverSubtractedAlpha: refiners.recalibrateOverSubtractedAlpha,
107
+ fineTuneDarkCatalogAlpha: refiners.fineTuneDarkCatalogAlpha,
104
108
  fineTuneWeakPositiveResidualAlpha: refiners.fineTuneWeakPositiveResidualAlpha
105
109
  }
106
110
  });
@@ -27,12 +27,92 @@ export function createFineAlphaTrialSequenceSpecs({
27
27
  refiners = {}
28
28
  } = {}) {
29
29
  const {
30
+ refineLargeMargin48ProfileAlphaRescue,
31
+ fineTuneEvidenceGatedLocalAlpha,
30
32
  recalibrateOverSubtractedAlpha,
31
33
  fineTuneDarkCatalogAlpha,
32
34
  fineTuneWeakPositiveResidualAlpha
33
35
  } = refiners;
34
36
 
37
+ const evidenceGatedStages = [];
38
+ if (typeof refineLargeMargin48ProfileAlphaRescue === 'function') {
39
+ evidenceGatedStages.push({
40
+ stage: 'large-margin-48-profile-alpha-rescue',
41
+ strategy: 'large-margin-48-profile-alpha',
42
+ createTrial: () => {
43
+ const state = readPipelineAlphaState(readState);
44
+ return typeof refineLargeMargin48ProfileAlphaRescue === 'function'
45
+ ? refineLargeMargin48ProfileAlphaRescue({
46
+ originalImageData,
47
+ currentImageData: state.finalImageData,
48
+ currentAlphaMap: state.alphaMap,
49
+ currentPosition: state.position,
50
+ currentConfig: state.config,
51
+ currentSpatialScore: state.finalProcessedSpatialScore,
52
+ currentGradientScore: state.finalProcessedGradientScore,
53
+ currentAlphaGain: state.alphaGain,
54
+ originalSpatialScore,
55
+ originalGradientScore
56
+ })
57
+ : null;
58
+ },
59
+ acceptCurrentAlphaTrialResult,
60
+ source: () => {
61
+ const source = readPipelineAlphaState(readState).source;
62
+ return source.includes('+profile-alpha-rescue')
63
+ ? source
64
+ : `${source}+profile-alpha-rescue`;
65
+ },
66
+ stageExtras: (result) => ({
67
+ profileExponent: result.profileExponent
68
+ })
69
+ });
70
+ }
71
+ if (typeof fineTuneEvidenceGatedLocalAlpha === 'function') {
72
+ evidenceGatedStages.push({
73
+ stage: 'evidence-gated-local-alpha-search',
74
+ strategy: 'evidence-gated-local-alpha',
75
+ createTrial: () => {
76
+ const state = readPipelineAlphaState(readState);
77
+ return typeof fineTuneEvidenceGatedLocalAlpha === 'function'
78
+ ? fineTuneEvidenceGatedLocalAlpha({
79
+ originalImageData,
80
+ currentImageData: state.finalImageData,
81
+ alphaMap: state.alphaMap,
82
+ position: state.position,
83
+ currentSpatialScore: state.finalProcessedSpatialScore,
84
+ currentGradientScore: state.finalProcessedGradientScore,
85
+ currentAlphaGain: state.alphaGain,
86
+ originalSpatialScore,
87
+ originalGradientScore,
88
+ originalNearBlackRatio: resolveNearBlackRatio({
89
+ calculateNearBlackRatio,
90
+ imageData: originalImageData,
91
+ position: state.position
92
+ })
93
+ })
94
+ : null;
95
+ },
96
+ acceptCurrentAlphaTrialResult,
97
+ source: () => {
98
+ const source = readPipelineAlphaState(readState).source;
99
+ return source.includes('+fine-alpha')
100
+ ? source
101
+ : `${source}+fine-alpha`;
102
+ },
103
+ stageExtras: (result) => ({
104
+ localSearchTrigger: result.localSearchTrigger,
105
+ darkBackgroundSupportConvergence:
106
+ result.darkBackgroundSupportConvergence ?? null
107
+ }),
108
+ debugTimings,
109
+ timingKey: debugTimingsEnabled ? 'localAlphaSearchMs' : null,
110
+ nowMs
111
+ });
112
+ }
113
+
35
114
  return [
115
+ ...evidenceGatedStages,
36
116
  {
37
117
  stage: 'over-subtraction-recalibration',
38
118
  strategy: 'over-subtraction-fine-alpha',
@@ -105,6 +185,7 @@ export function createFineAlphaTrialSequenceSpecs({
105
185
  return typeof fineTuneWeakPositiveResidualAlpha === 'function'
106
186
  ? fineTuneWeakPositiveResidualAlpha({
107
187
  originalImageData,
188
+ currentImageData: state.finalImageData,
108
189
  alphaMap: state.alphaMap,
109
190
  position: state.position,
110
191
  currentSpatialScore: state.finalProcessedSpatialScore,
@@ -21,13 +21,15 @@ export function normalizeAlphaAdjustmentStageForTrace(stagePayload = {}) {
21
21
  profileExponent = null,
22
22
  alphaStrategy = null,
23
23
  repairStrategy = null,
24
- allowSameAlphaGain = false
24
+ allowSameAlphaGain = false,
25
+ ...stageExtras
25
26
  } = stagePayload;
26
27
 
27
28
  if (!stage || !Number.isFinite(fromAlphaGain) || !Number.isFinite(toAlphaGain)) return null;
28
29
  if (!allowSameAlphaGain && Math.abs(fromAlphaGain - toAlphaGain) < 0.0001) return null;
29
30
 
30
31
  return {
32
+ ...stageExtras,
31
33
  stage,
32
34
  fromAlphaGain,
33
35
  toAlphaGain,