Skip to content

Commit d149292

Browse files
authored
Merge pull request #80 from Jalen-Stephens/54-feature-expand-analyze-to-correctly-identify-screen-shots
feat(c2pa): flag screenshots and surface in analyze responses
2 parents c11a95d + b410a60 commit d149292

File tree

12 files changed

+285
-14
lines changed

12 files changed

+285
-14
lines changed

citations.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,53 @@
1+
### **Commit / Ticket Reference**
2+
- **Commit:** feat(c2pa): flag screenshots and surface in analyze responses
3+
- **Ticket:** none (Feature #54)
4+
- **Date:** 2025-12-02
5+
- **Team Member:** Jalen Stephens
6+
7+
---
8+
9+
### **AI Tool Information**
10+
- **Tool Used:** OpenAI ChatGPT (GPT-5) via Codex CLI
11+
- **Access Method:** Local Codex CLI session (sandboxed; no paid API calls)
12+
- **Configuration:** Default model settings
13+
- **Cost:** $0 (course-provided access)
14+
15+
---
16+
17+
### **Purpose of AI Assistance**
18+
Added C2PA-driven screenshot detection and plumbing so screenshots are flagged as suspicious and exposed to clients. Work included heuristics based on claim generator and capture_type, new metadata fields, feature-vector update, score bump for screenshots, response DTO extensions, and regression tests.
19+
20+
---
21+
22+
### **Prompts / Interaction Summary**
23+
- “Implement Feature #54: Expand Analyze to correctly identify screen shots”
24+
- “Plumb screenshot flag into analyze pipeline and response DTO”
25+
- “Add JSON parsing and service tests for screenshot detection”
26+
- “Fix Checkstyle line length in AnalyzeServiceTest”
27+
28+
---
29+
30+
### **Resulting Artifacts**
31+
- `src/main/java/dev/coms4156/project/metadetect/c2pa/C2paToolInvoker.java`
32+
- `src/main/java/dev/coms4156/project/metadetect/service/FeatureExtractor.java`
33+
- `src/main/java/dev/coms4156/project/metadetect/service/AnalyzeService.java`
34+
- `src/main/java/dev/coms4156/project/metadetect/dto/Dtos.java`
35+
- `src/main/resources/model.json`
36+
- Tests: `src/test/java/dev/coms4156/project/metadetect/c2pa/C2paToolInvokerJsonParsingTest.java`, `src/test/java/dev/coms4156/project/metadetect/service/AnalyzeServiceTest.java`, `src/test/java/dev/coms4156/project/metadetect/service/FeatureExtractorTest.java`, `src/test/java/dev/coms4156/project/metadetect/controller/AnalyzeControllerTest.java`, `src/test/java/dev/coms4156/project/metadetect/dto/DtosTest.java`
37+
38+
---
39+
40+
### **Verification**
41+
- `./mvnw -q test` (passes; expected warnings from mocked c2patool branches)
42+
- `./mvnw -q -DskipTests compile` to regenerate main classes after changes
43+
44+
---
45+
46+
### **Attribution Statement**
47+
> Portions of this work were generated with assistance from OpenAI ChatGPT (GPT-5) on 2025-12-02. All AI-generated content was reviewed and finalized by the development team.
48+
49+
---
50+
151
### **Commit / Ticket Reference**
252
- **Commit:** feat(logging): add request logging filter and console logback config
353
- **Ticket:** none

src/main/java/dev/coms4156/project/metadetect/c2pa/C2paToolInvoker.java

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,25 @@ public class C2paToolInvoker {
3838
"runway"
3939
);
4040

41+
/**
42+
* Keywords used to conservatively infer that an asset is a screenshot.
43+
* This list intentionally focuses on obvious OS/browser capture tools.
44+
*/
45+
private static final List<String> SCREENSHOT_KEYWORDS = Arrays.asList(
46+
"screenshot",
47+
"screen shot",
48+
"screen capture",
49+
"screen-capture",
50+
"screen grab",
51+
"screen-grab",
52+
"snipping tool",
53+
"snip & sketch",
54+
"snip and sketch",
55+
"windows snip",
56+
"macos screenshot",
57+
"screencapture"
58+
);
59+
4160
private final String c2paToolPath;
4261

4362
/**
@@ -191,6 +210,9 @@ private C2paMetadata parseMetadataFromJson(String json) {
191210
int hasManifest = 0;
192211
String claimGenerator = null;
193212
int claimGeneratorIsAi = 0;
213+
int c2paIsScreenshot = 0;
214+
String c2paScreenshotReason = null;
215+
JsonNode claimNode = null;
194216

195217
// Count manifests
196218
JsonNode manifestsNode = root.get("manifests");
@@ -220,7 +242,7 @@ private C2paMetadata parseMetadataFromJson(String json) {
220242
}
221243

222244
if (activeManifestNode != null && !activeManifestNode.isMissingNode()) {
223-
JsonNode claimNode = activeManifestNode.get("claim");
245+
claimNode = activeManifestNode.get("claim");
224246
if (claimNode == null || claimNode.isMissingNode()) {
225247
claimNode = activeManifestNode;
226248
}
@@ -248,11 +270,38 @@ && isAiClaimGenerator(claimGenerator)) {
248270
claimGeneratorIsAi = 0;
249271
}
250272

273+
// Heuristic: detect screenshot tools via claim generator or capture_type.
274+
if (claimGenerator != null && !claimGenerator.isBlank()) {
275+
String gen = claimGenerator.toLowerCase();
276+
for (String kw : SCREENSHOT_KEYWORDS) {
277+
if (gen.contains(kw)) {
278+
c2paIsScreenshot = 1;
279+
c2paScreenshotReason = "claim_generator contains \"" + kw + "\"";
280+
break;
281+
}
282+
}
283+
}
284+
if (c2paIsScreenshot == 0 && claimNode != null) {
285+
JsonNode captureType = claimNode.get("capture_type");
286+
if (captureType == null) {
287+
captureType = claimNode.get("captureType");
288+
}
289+
String captureVal = captureType != null && !captureType.isNull()
290+
? captureType.asText(null)
291+
: null;
292+
if (captureVal != null && captureVal.equalsIgnoreCase("screenshot")) {
293+
c2paIsScreenshot = 1;
294+
c2paScreenshotReason = "capture_type indicates screenshot";
295+
}
296+
}
297+
251298
return new C2paMetadata(
252299
hasManifest,
253300
manifestCount,
254301
claimGenerator,
255302
claimGeneratorIsAi,
303+
c2paIsScreenshot,
304+
c2paScreenshotReason,
256305
/*c2paErrorFlag*/ 0,
257306
/*c2paErrorMessage*/ null
258307
);
@@ -298,6 +347,8 @@ private static boolean isAiClaimGenerator(String generator) {
298347
"c2paManifestCount",
299348
"c2paClaimGenerator",
300349
"c2paClaimGeneratorIsAi",
350+
"c2paIsScreenshot",
351+
"c2paScreenshotReason",
301352
"c2paErrorFlag",
302353
"c2paErrorMessage"
303354
})
@@ -307,6 +358,8 @@ public static final class C2paMetadata {
307358
private final int c2paManifestCount;
308359
private final String c2paClaimGenerator;
309360
private final int c2paClaimGeneratorIsAi;
361+
private final int c2paIsScreenshot;
362+
private final String c2paScreenshotReason;
310363
private final int c2paErrorFlag;
311364
private final String c2paErrorMessage;
312365

@@ -317,6 +370,8 @@ public static final class C2paMetadata {
317370
* @param c2paManifestCount number of manifests detected in the file
318371
* @param c2paClaimGenerator claim generator string from the active manifest
319372
* @param c2paClaimGeneratorIsAi integer flag: 1 if AI generator detected, 0 if not
373+
* @param c2paIsScreenshot integer flag: 1 if screenshot heuristics matched, 0 otherwise
374+
* @param c2paScreenshotReason optional note describing which heuristic fired
320375
* @param c2paErrorFlag integer flag: 1 if error occurred, 0 if no error
321376
* @param c2paErrorMessage error message if errorFlag=1, null otherwise
322377
*/
@@ -325,13 +380,17 @@ public C2paMetadata(
325380
int c2paManifestCount,
326381
String c2paClaimGenerator,
327382
int c2paClaimGeneratorIsAi,
383+
int c2paIsScreenshot,
384+
String c2paScreenshotReason,
328385
int c2paErrorFlag,
329386
String c2paErrorMessage) {
330387

331388
this.c2paHasManifest = c2paHasManifest;
332389
this.c2paManifestCount = c2paManifestCount;
333390
this.c2paClaimGenerator = c2paClaimGenerator;
334391
this.c2paClaimGeneratorIsAi = c2paClaimGeneratorIsAi;
392+
this.c2paIsScreenshot = c2paIsScreenshot;
393+
this.c2paScreenshotReason = c2paScreenshotReason;
335394
this.c2paErrorFlag = c2paErrorFlag;
336395
this.c2paErrorMessage = c2paErrorMessage;
337396
}
@@ -343,6 +402,8 @@ public static C2paMetadata noManifest() {
343402
/*c2paManifestCount*/ 0,
344403
/*c2paClaimGenerator*/ null,
345404
/*c2paClaimGeneratorIsAi*/ 0,
405+
/*c2paIsScreenshot*/ 0,
406+
/*c2paScreenshotReason*/ null,
346407
/*c2paErrorFlag*/ 0,
347408
/*c2paErrorMessage*/ null
348409
);
@@ -355,6 +416,8 @@ public static C2paMetadata error(String message) {
355416
/*c2paManifestCount*/ 0,
356417
/*c2paClaimGenerator*/ null,
357418
/*c2paClaimGeneratorIsAi*/ 0,
419+
/*c2paIsScreenshot*/ 0,
420+
/*c2paScreenshotReason*/ null,
358421
/*c2paErrorFlag*/ 1,
359422
/*c2paErrorMessage*/ message
360423
);
@@ -376,6 +439,14 @@ public int getc2paClaimGeneratorIsAi() {
376439
return c2paClaimGeneratorIsAi;
377440
}
378441

442+
public int getc2paIsScreenshot() {
443+
return c2paIsScreenshot;
444+
}
445+
446+
public String getc2paScreenshotReason() {
447+
return c2paScreenshotReason;
448+
}
449+
379450
public int getc2paErrorFlag() {
380451
return c2paErrorFlag;
381452
}

src/main/java/dev/coms4156/project/metadetect/dto/Dtos.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ public record AnalyzeConfidenceResponse(
6969
String status,
7070
Double confidenceScore,
7171
boolean c2paUsed,
72-
String modelVersion
72+
String modelVersion,
73+
boolean isScreenshot,
74+
String screenshotReason
7375
) { }
7476

7577
/**

src/main/java/dev/coms4156/project/metadetect/service/AnalyzeService.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public class AnalyzeService {
4848
private final UserService userService;
4949
private final LogisticRegressionService logisticRegressionService;
5050
private final Clock clock;
51+
private static final double SCREENSHOT_SUSPICION_BONUS = 0.05;
5152

5253
// Lightweight mapper for error JSON assembly.
5354
private final ObjectMapper objectMapper = new ObjectMapper();
@@ -174,12 +175,16 @@ public Dtos.AnalyzeConfidenceResponse getConfidence(UUID analysisId) {
174175
var currentUser = userService.getCurrentUserIdOrThrow();
175176
imageService.getById(currentUser, report.getImageId());
176177

178+
ScreenshotInfo screenshotInfo = deriveScreenshotInfo(report.getDetails());
179+
177180
return new Dtos.AnalyzeConfidenceResponse(
178181
report.getId().toString(),
179182
report.getStatus().name(),
180183
report.getConfidence(),
181184
deriveC2paUsed(report.getDetails()),
182-
logisticRegressionService.getModelVersion()
185+
logisticRegressionService.getModelVersion(),
186+
screenshotInfo.isScreenshot(),
187+
screenshotInfo.reason()
183188
);
184189
}
185190

@@ -228,12 +233,16 @@ private void runExtractionAndFinalize(UUID analysisId, String storagePath) {
228233
tempFile.getAbsolutePath(),
229234
meta
230235
);
236+
double adjustedConfidence = applyScreenshotAdjustment(
237+
inference.confidenceScore(),
238+
meta
239+
);
231240

232241
// 4) Serialize metadata and mark COMPLETED with a confidence score
233242
String json = objectMapper.writeValueAsString(meta);
234243

235244
// The details field now stores the C2PA metadata schema, not raw manifest JSON.
236-
markCompleted(analysisId, json, inference.confidenceScore());
245+
markCompleted(analysisId, json, adjustedConfidence);
237246

238247
} catch (IOException ioe) {
239248
// IO-level failures (download, JSON serialization) are genuine failures.
@@ -335,6 +344,34 @@ private boolean deriveC2paUsed(String detailsJson) {
335344
}
336345
}
337346

347+
private ScreenshotInfo deriveScreenshotInfo(String detailsJson) {
348+
if (!StringUtils.hasText(detailsJson)) {
349+
return new ScreenshotInfo(false, null);
350+
}
351+
try {
352+
JsonNode node = objectMapper.readTree(detailsJson);
353+
boolean isScreenshot = node.path("c2paIsScreenshot").asInt(0) == 1;
354+
String reason = node.path("c2paScreenshotReason").asText(null);
355+
if (isScreenshot && (reason == null || reason.isBlank())) {
356+
String generator = node.path("c2paClaimGenerator").asText(null);
357+
if (StringUtils.hasText(generator)) {
358+
reason = "Inferred from claim generator: " + generator;
359+
}
360+
}
361+
return new ScreenshotInfo(isScreenshot, reason);
362+
} catch (Exception e) {
363+
return new ScreenshotInfo(false, null);
364+
}
365+
}
366+
367+
private double applyScreenshotAdjustment(double baseScore, C2paToolInvoker.C2paMetadata meta) {
368+
if (meta != null && meta.getc2paIsScreenshot() == 1) {
369+
double bumped = baseScore + SCREENSHOT_SUSPICION_BONUS;
370+
return bumped > 1.0 ? 1.0 : bumped;
371+
}
372+
return baseScore;
373+
}
374+
338375
/** Truncates a string to a maximum length, null-safe. */
339376
private static String truncate(String s, int max) {
340377
if (s == null) {
@@ -370,6 +407,8 @@ static AnalysisReport pending(UUID imageId, Instant createdAt) {
370407
}
371408
}
372409

410+
private record ScreenshotInfo(boolean isScreenshot, String reason) { }
411+
373412
/**
374413
* Handles a generic failure during analysis by marking the analysis as FAILED
375414
* and storing the error message in the details field.

src/main/java/dev/coms4156/project/metadetect/service/FeatureExtractor.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public double[] extractAllFeatures(String imagePath, C2paMetadata c2pa) {
8484
c2pa.getc2paHasManifest(),
8585
c2pa.getc2paManifestCount(),
8686
c2pa.getc2paClaimGeneratorIsAi(),
87+
c2pa.getc2paIsScreenshot(),
8788
c2pa.getc2paErrorFlag()
8889
};
8990
}
@@ -100,6 +101,7 @@ private double[] generateEmpty(C2paMetadata c2pa) {
100101
c2pa.getc2paHasManifest(),
101102
c2pa.getc2paManifestCount(),
102103
c2pa.getc2paClaimGeneratorIsAi(),
104+
c2pa.getc2paIsScreenshot(),
103105
c2pa.getc2paErrorFlag()
104106
};
105107
}

src/main/resources/model.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
0.0,
1313
0.0,
1414
0.0,
15+
0.35,
1516
0.0
1617
],
1718
"bias": -13.851754449616177
18-
}
19+
}

0 commit comments

Comments
 (0)