diff --git a/package-lock.json b/package-lock.json
index f4da796a9..b461ffe9f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@mui/icons-material": "^5.14.12",
"@mui/lab": "^5.0.0-alpha.147",
"@mui/material": "^5.14.12",
+ "@mui/x-data-grid": "^7.9.0",
"@mui/x-date-pickers": "^6.16.1",
"@reduxjs/toolkit": "^1.9.7",
"@types/chrome": "^0.0.266",
@@ -1912,9 +1913,9 @@
"dev": true
},
"node_modules/@babel/runtime": {
- "version": "7.24.5",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
- "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz",
+ "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -3010,12 +3011,12 @@
}
},
"node_modules/@mui/private-theming": {
- "version": "5.15.14",
- "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz",
- "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==",
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.0.tgz",
+ "integrity": "sha512-sYpubkO1MZOnxNyVOClrPNOTs0MfuRVVnAvCeMaOaXt6GimgQbnUcshYv2pSr6PFj+Mqzdff/FYOBceK8u5QgA==",
"dependencies": {
"@babel/runtime": "^7.23.9",
- "@mui/utils": "^5.15.14",
+ "@mui/utils": "^5.16.0",
"prop-types": "^15.8.1"
},
"engines": {
@@ -3067,15 +3068,15 @@
}
},
"node_modules/@mui/system": {
- "version": "5.15.15",
- "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.15.tgz",
- "integrity": "sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==",
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.0.tgz",
+ "integrity": "sha512-9YbkC2m3+pNumAvubYv+ijLtog6puJ0fJ6rYfzfLCM47pWrw3m+30nXNM8zMgDaKL6vpfWJcCXm+LPaWBpy7sw==",
"dependencies": {
"@babel/runtime": "^7.23.9",
- "@mui/private-theming": "^5.15.14",
+ "@mui/private-theming": "^5.16.0",
"@mui/styled-engine": "^5.15.14",
"@mui/types": "^7.2.14",
- "@mui/utils": "^5.15.14",
+ "@mui/utils": "^5.16.0",
"clsx": "^2.1.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
@@ -3119,9 +3120,9 @@
}
},
"node_modules/@mui/utils": {
- "version": "5.15.14",
- "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz",
- "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==",
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.0.tgz",
+ "integrity": "sha512-kLLi5J1xY+mwtUlMb8Ubdxf4qFAA1+U7WPBvjM/qQ4CIwLCohNb0sHo1oYPufjSIH/Z9+dhVxD7dJlfGjd1AVA==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@types/prop-types": "^15.7.11",
@@ -3145,6 +3146,32 @@
}
}
},
+ "node_modules/@mui/x-data-grid": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.9.0.tgz",
+ "integrity": "sha512-RkrVD+tfcR/h3j2p2uqohxA00C5tCJIV5gb5+2ap8XdM0Y8XMF81bB8UADWenU5W83UTErWvtU7n4gCl7hJO9g==",
+ "dependencies": {
+ "@babel/runtime": "^7.24.7",
+ "@mui/system": "^5.16.0",
+ "@mui/utils": "^5.16.0",
+ "@mui/x-internals": "7.9.0",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "reselect": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@mui/material": "^5.15.14",
+ "react": "^17.0.0 || ^18.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@mui/x-date-pickers": {
"version": "6.19.9",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.19.9.tgz",
@@ -3210,6 +3237,25 @@
}
}
},
+ "node_modules/@mui/x-internals": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.9.0.tgz",
+ "integrity": "sha512-RJRrM6moaDZ8S11gDt8OKVclKm2v9khpIyLkpenNze+tT4dQYoU3liW5P2t31hA4Na/T6JQKNosB4qmB2TYfZw==",
+ "dependencies": {
+ "@babel/runtime": "^7.24.7",
+ "@mui/utils": "^5.16.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
diff --git a/package.json b/package.json
index b58cafab7..1f65a0300 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@mui/icons-material": "^5.14.12",
"@mui/lab": "^5.0.0-alpha.147",
"@mui/material": "^5.14.12",
+ "@mui/x-data-grid": "^7.9.0",
"@mui/x-date-pickers": "^6.16.1",
"@reduxjs/toolkit": "^1.9.7",
"@types/chrome": "^0.0.266",
diff --git a/src/components/NavItems/tools/Alltools/ToolsMenu.jsx b/src/components/NavItems/tools/Alltools/ToolsMenu.jsx
index c45e21b59..37da4c8fe 100644
--- a/src/components/NavItems/tools/Alltools/ToolsMenu.jsx
+++ b/src/components/NavItems/tools/Alltools/ToolsMenu.jsx
@@ -30,7 +30,8 @@ import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { i18nLoadNamespace } from "components/Shared/Languages/i18nLoadNamespace";
import { Audiotrack } from "@mui/icons-material";
-import { ROLES, tools, TOOLS_CATEGORIES } from "../../../../constants/tools";
+import { tools, TOOLS_CATEGORIES } from "../../../../constants/tools";
+import { ROLES } from "../../../../constants/roles";
function TabPanel(props) {
const { children, value, index, ...other } = props;
diff --git a/src/components/NavItems/tools/SyntheticImageDetection/NddDatagrid.jsx b/src/components/NavItems/tools/SyntheticImageDetection/NddDatagrid.jsx
new file mode 100644
index 000000000..0755567b9
--- /dev/null
+++ b/src/components/NavItems/tools/SyntheticImageDetection/NddDatagrid.jsx
@@ -0,0 +1,257 @@
+import * as React from "react";
+import Box from "@mui/material/Box";
+import { DataGrid, GridActionsCellItem } from "@mui/x-data-grid";
+import Link from "@mui/material/Link";
+import { Chip, Grid, Stack, Typography } from "@mui/material";
+import { i18nLoadNamespace } from "../../../Shared/Languages/i18nLoadNamespace";
+import {
+ getAlertColor,
+ getAlertLabel,
+ getPercentageColorCode,
+} from "./syntheticImageDetectionResults";
+import { OpenInNew } from "@mui/icons-material";
+import { useSelector } from "react-redux";
+import { createTheme, ThemeProvider } from "@mui/material/styles";
+
+import {
+ arSD,
+ deDE,
+ elGR,
+ enUS,
+ esES,
+ frFR,
+ itIT,
+} from "@mui/x-data-grid/locales";
+
+const languages = {
+ en: enUS,
+ fr: frFR,
+ es: esES,
+ el: elGR,
+ it: itIT,
+ ar: arSD,
+ de: deDE,
+};
+
+const NddDataGrid = ({ rows }) => {
+ const keyword = i18nLoadNamespace(
+ "components/NavItems/tools/SyntheticImageDetection",
+ );
+
+ const currentLang = useSelector((state) => state.language);
+ const isCurrentLanguageLeftToRight = currentLang !== "ar";
+
+ //Retrieves the localization for the Datagrid
+ const datagridLanguage = languages[currentLang] || languages["en"];
+
+ const detectionRateStack = (row, index) => {
+ if (!row.detectionResults[index]) return null;
+
+ const detectionPercentageNdImage =
+ row.detectionResults[index].predictionScore;
+
+ return (
+
+
+ <>
+
+ {keyword("synthetic_image_detection_probability_text")}{" "}
+
+
+ {detectionPercentageNdImage}%
+
+ >
+
+
+
+ );
+ };
+
+ const imageUrlsCell = (urls) => {
+ if (!urls || !Array.isArray(urls) || urls.length === 0) return <>>;
+
+ return (
+
+ {urls.map((url, index) => (
+
+
+ {`#${index + 1}`}
+
+
+ ))}
+
+ );
+ };
+
+ /**
+ * Computes the JSX element to display for the algorithm name cell
+ * @param params
+ * @param index {number} The array position
+ * @returns {React.JSX.Element|null} The JSX element if applicable else null
+ */
+ const renderAlgorithmName = (params, index) => {
+ if (!params.row.detectionResults[index]) {
+ return null;
+ }
+
+ return (
+
+ {keyword(params.row.detectionResults[index].name)}
+
+ );
+ };
+
+ /**
+ * A helper function to compute the detection rows as not all the NDD results have the same number of algorithms for detections
+ * @returns {*[]}
+ */
+ const detectionDetailsRows = () => {
+ const maxSizeAlgorithm = Math.max(
+ ...rows.map((row) => row.detectionResults.length),
+ );
+
+ let algorithmsRows = [];
+
+ for (let i = 0; i < maxSizeAlgorithm; i++) {
+ algorithmsRows.push({
+ field: `detectionName${i + 1}`,
+ headerName: `${keyword(
+ "synthetic_image_detection_ndd_table_header_3",
+ )}${i + 1}`,
+ type: "string",
+ minWidth: 120,
+ valueGetter: (value, row) => row?.detectionResults[i]?.name,
+ renderCell: (params) => renderAlgorithmName(params, i),
+ });
+
+ algorithmsRows.push({
+ field: `detectionRate${i + 1}`,
+ headerName: `${keyword(
+ "synthetic_image_detection_ndd_table_header_4",
+ )}${i + 1}`,
+ type: "number",
+ minWidth: 180,
+ valueGetter: (value, row) => row?.detectionResults[i]?.predictionScore,
+ renderCell: (params) => detectionRateStack(params.row, i),
+ });
+ }
+
+ return algorithmsRows;
+ };
+
+ const columns = [
+ {
+ field: "id",
+ headerName: keyword("synthetic_image_detection_ndd_table_header_1"),
+ width: 10,
+ },
+ {
+ field: "image",
+ headerName: keyword("synthetic_image_detection_ndd_table_header_2"),
+ minWidth: 150,
+ sortable: false,
+ renderCell: (params) =>
,
+ },
+ ...detectionDetailsRows(),
+ {
+ field: "archiveUrl",
+ headerName: keyword("synthetic_image_detection_ndd_table_header_5"),
+ sortable: false,
+ renderCell: (params) => (
+
+ {params.value}
+
+ ),
+ },
+ {
+ field: "imageUrls",
+ headerName: keyword("synthetic_image_detection_ndd_table_header_6"),
+ sortable: false,
+ renderCell: (params) => imageUrlsCell(params.value),
+ },
+ {
+ headerName: keyword("synthetic_image_detection_ndd_table_header_7"),
+ field: "actions",
+ type: "actions",
+ width: 120,
+ getActions: (params) => {
+ const url = new URL(
+ window.location.href + "?url=" + params.row.archiveUrl,
+ );
+ return [
+ // eslint-disable-next-line react/jsx-key
+ }
+ onClick={() => window.open(url, "_blank", "noopener noreferrer")}
+ label="Open Analysis"
+ />,
+ ];
+ },
+ },
+ ];
+
+ /* This is needed to fix the pagination arrows for RTL languages
+ * See https://mui.com/x/react-data-grid/localization/#rtl-support
+ */
+ const theme = createTheme(
+ {
+ direction: isCurrentLanguageLeftToRight ? "ltr" : "rtl",
+ palette: {
+ primary: {
+ light: "#00926c",
+ main: "#00926c",
+ dark: "#00926c",
+ contrastText: "#fff",
+ },
+ },
+ },
+ datagridLanguage,
+ );
+
+ return (
+
+
+ "auto"}
+ rows={rows}
+ columns={columns}
+ initialState={{
+ pagination: {
+ paginationModel: {
+ pageSize: 5,
+ },
+ },
+ }}
+ autosizeOnMount={true}
+ autosizeOptions={{
+ includeHeaders: true,
+ includeOutliers: true,
+ expand: true,
+ outliersFactor: 20,
+ }}
+ pageSizeOptions={[5]}
+ disableRowSelectionOnClick
+ />
+
+
+ );
+};
+
+export default NddDataGrid;
diff --git a/src/components/NavItems/tools/SyntheticImageDetection/SyntheticImageDetectionAlgorithms.jsx b/src/components/NavItems/tools/SyntheticImageDetection/SyntheticImageDetectionAlgorithms.jsx
new file mode 100644
index 000000000..7bd1ec673
--- /dev/null
+++ b/src/components/NavItems/tools/SyntheticImageDetection/SyntheticImageDetectionAlgorithms.jsx
@@ -0,0 +1,139 @@
+import { ROLES } from "../../../../constants/roles";
+
+/**
+ * @file Provides constants and helper functions used for the synthetic image detection tool
+ *
+ */
+
+/**
+ * Thresholds (percentage) for the different analysis categories
+ * @type {{THRESHOLD_2: number, THRESHOLD_3: number, THRESHOLD_1: number}}
+ */
+export const DETECTION_THRESHOLDS = {
+ THRESHOLD_1: 50,
+ THRESHOLD_2: 70,
+ THRESHOLD_3: 90,
+};
+
+export class SyntheticImageDetectionAlgorithm {
+ /**
+ *
+ * @param apiServiceName {string} The service parameter for the API call
+ * @param name {string} The algorithm name key
+ * @param description {string} The algorithm description key
+ * @param roleNeeded {?Roles} Role needed to get the detection results for the algorithm
+ */
+ constructor(apiServiceName, name, description, roleNeeded) {
+ this.apiServiceName = apiServiceName;
+ this.name = name;
+ this.description = description;
+ this.roleNeeded = roleNeeded;
+ }
+}
+
+export const ganR50Mever = new SyntheticImageDetectionAlgorithm(
+ "gan_r50_mever",
+ "synthetic_image_detection_gan_name",
+ "synthetic_image_detection_gan_description",
+ ROLES.BETA_TESTER,
+);
+
+export const proGanR50Grip = new SyntheticImageDetectionAlgorithm(
+ "progan_r50_grip",
+ "synthetic_image_detection_progan_name",
+ "synthetic_image_detection_progan_description",
+ ROLES.BETA_TESTER,
+);
+
+export const ldmR50Grip = new SyntheticImageDetectionAlgorithm(
+ "ldm_r50_grip",
+ "synthetic_image_detection_diffusion_name",
+ "synthetic_image_detection_diffusion_description",
+ ROLES.BETA_TESTER,
+);
+
+export const proGanWebpR50Grip = new SyntheticImageDetectionAlgorithm(
+ "progan-webp_r50_grip",
+ "synthetic_image_detection_progan-webp_r50_grip_name",
+ "synthetic_image_detection_progan-webp_r50_grip_description",
+ ROLES.EXTRA_FEATURE,
+);
+
+export const ldmWebpR50Grip = new SyntheticImageDetectionAlgorithm(
+ "ldm-webp_r50_grip",
+ "synthetic_image_detection_ldm-webp_r50_grip_name",
+ "synthetic_image_detection_ldm-webp_r50_grip_description",
+ ROLES.EXTRA_FEATURE,
+);
+
+export const gigaGanWebpR50Grip = new SyntheticImageDetectionAlgorithm(
+ "gigagan-webp_r50_grip",
+ "synthetic_image_detection_gigagan-webp_r50_grip_name",
+ "synthetic_image_detection_gigagan-webp_r50_grip_description",
+ ROLES.EXTRA_FEATURE,
+);
+
+export const admR50Grip = new SyntheticImageDetectionAlgorithm(
+ "adm_r50_grip",
+ "synthetic_image_detection_adm_name",
+ "synthetic_image_detection_adm_description",
+ ROLES.EXTRA_FEATURE,
+);
+export const proGanRineMever = new SyntheticImageDetectionAlgorithm(
+ "progan_rine_mever",
+ "synthetic_image_detection_progan_rine_mever_name",
+ "synthetic_image_detection_progan_rine_mever_description",
+ ROLES.EXTRA_FEATURE,
+);
+export const ldmRineMever = new SyntheticImageDetectionAlgorithm(
+ "ldm_rine_mever",
+ "synthetic_image_detection_ldm_rine_mever_name",
+ "synthetic_image_detection_ldm_rine_mever_description",
+ ROLES.EXTRA_FEATURE,
+);
+export const ldmR50Mever = new SyntheticImageDetectionAlgorithm(
+ "ldm_r50_mever",
+ "synthetic_image_detection_ldm_r50_mever_name",
+ "synthetic_image_detection_ldm_r50_mever_description",
+ ROLES.EXTRA_FEATURE,
+);
+
+/**
+ * The list of the synthetic image detection algorithms
+ * TODO:Use SET
+ * @type {SyntheticImageDetectionAlgorithm[]}
+ */
+export const syntheticImageDetectionAlgorithms = [
+ proGanR50Grip,
+ ldmR50Grip,
+ admR50Grip,
+ proGanWebpR50Grip,
+ ldmWebpR50Grip,
+ gigaGanWebpR50Grip,
+ ganR50Mever,
+ ldmR50Mever,
+ proGanRineMever,
+ ldmRineMever,
+];
+
+/**
+ * Returns a list of algorithms that can be used by a user according to their roles
+ * @param roles {?Array}
+ * @returns {SyntheticImageDetectionAlgorithm[]}
+ */
+export const getSyntheticImageDetectionAlgorithmsForRoles = (roles) => {
+ return syntheticImageDetectionAlgorithms.filter((algorithm) =>
+ roles.includes(algorithm.roleNeeded),
+ );
+};
+
+/**
+ * Returns the Synthetic Image Detection Algorithm Object from the given APi Service Name
+ * @param apiName {string}
+ * @returns {SyntheticImageDetectionAlgorithm}
+ */
+export const getSyntheticImageDetectionAlgorithmFromApiName = (apiName) => {
+ return syntheticImageDetectionAlgorithms.find(
+ (algorithm) => algorithm.apiServiceName === apiName,
+ );
+};
diff --git a/src/components/NavItems/tools/SyntheticImageDetection/index.jsx b/src/components/NavItems/tools/SyntheticImageDetection/index.jsx
index b5796b00e..a4c922cff 100644
--- a/src/components/NavItems/tools/SyntheticImageDetection/index.jsx
+++ b/src/components/NavItems/tools/SyntheticImageDetection/index.jsx
@@ -1,8 +1,9 @@
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
resetSyntheticImageDetectionImage,
setSyntheticImageDetectionLoading,
+ setSyntheticImageDetectionNearDuplicates,
setSyntheticImageDetectionResult,
} from "../../../../redux/actions/tools/syntheticImageDetectionActions";
@@ -28,8 +29,14 @@ import SyntheticImageDetectionResults from "./syntheticImageDetectionResults";
import { setError } from "redux/reducers/errorReducer";
import StringFileUploadField from "../../../Shared/StringFileUploadField";
import { preprocessFileUpload } from "../../../Shared/Utils/fileUtils";
+import { syntheticImageDetectionAlgorithms } from "./SyntheticImageDetectionAlgorithms";
+import { useLocation } from "react-router-dom";
const SyntheticImageDetection = () => {
+ const location = useLocation();
+ const urlParams = new URLSearchParams(location.search);
+ const urlParam = urlParams.get("url");
+
const classes = useMyStyles();
const keyword = i18nLoadNamespace(
"components/NavItems/tools/SyntheticImageDetection",
@@ -46,6 +53,7 @@ const SyntheticImageDetection = () => {
);
const result = useSelector((state) => state.syntheticImageDetection.result);
const url = useSelector((state) => state.syntheticImageDetection.url);
+ const nd = useSelector((state) => state.syntheticImageDetection.duplicates);
const [input, setInput] = useState(url ? url : "");
const [imageFile, setImageFile] = useState(undefined);
@@ -69,8 +77,10 @@ const SyntheticImageDetection = () => {
dispatch(setSyntheticImageDetectionLoading(true));
const modeURL = "images/";
- const services =
- "gan_r50_mever,ldm_r50_grip,progan_r50_grip,adm_r50_grip,ldm_r50_mever,progan_rine_mever,ldm_rine_mever";
+
+ const services = syntheticImageDetectionAlgorithms
+ .map((algorithm) => algorithm.apiServiceName)
+ .join(",");
const baseURL = process.env.REACT_APP_CAA_DEEPFAKE_URL;
@@ -108,7 +118,7 @@ const SyntheticImageDetection = () => {
bodyFormData.append("file", image);
res = await axios.post(baseURL + modeURL + "jobs", bodyFormData, {
method: "post",
- params: { services: services },
+ params: { services: services, search_similar: true },
headers: {
"Content-Type": "multipart/form-data",
},
@@ -117,7 +127,7 @@ const SyntheticImageDetection = () => {
default:
res = await axios.post(baseURL + modeURL + "jobs", null, {
- params: { url: url, services: services },
+ params: { url: url, services: services, search_similar: true },
});
break;
}
@@ -136,13 +146,42 @@ const SyntheticImageDetection = () => {
handleError("error_" + error.status);
}
- if (response && response.data != null)
+ if (response && response.data != null) {
dispatch(
setSyntheticImageDetectionResult({
url: image ? URL.createObjectURL(image) : url,
result: response.data,
}),
);
+ }
+
+ if (
+ response &&
+ response.data &&
+ response.data.similar_images &&
+ response.data.similar_images.completed
+ ) {
+ let imgSimilarRes;
+
+ try {
+ imgSimilarRes = await axios.get(baseURL + modeURL + "similar/" + id);
+ } catch (error) {
+ handleError("error_" + error.status);
+ }
+
+ console.log(imgSimilarRes.data);
+
+ if (
+ !imgSimilarRes.data ||
+ !imgSimilarRes.data.similar_media ||
+ !Array.isArray(imgSimilarRes.data.similar_media) ||
+ imgSimilarRes.data.similar_media.length === 0
+ ) {
+ dispatch(setSyntheticImageDetectionNearDuplicates(null));
+ }
+
+ dispatch(setSyntheticImageDetectionNearDuplicates(imgSimilarRes.data));
+ }
};
const waitUntilFinish = async (id) => {
@@ -202,15 +241,29 @@ const SyntheticImageDetection = () => {
);
};
- const handleSubmit = async () => {
+ /**
+ *
+ * @param url {string}
+ * @returns {Promise}
+ */
+ const handleSubmit = async (url) => {
dispatch(resetSyntheticImageDetectionImage());
-
+ const urlInput = url ? url : input;
const type =
- input && typeof input === "string" ? IMAGE_FROM.URL : IMAGE_FROM.UPLOAD;
+ urlInput && typeof urlInput === "string"
+ ? IMAGE_FROM.URL
+ : IMAGE_FROM.UPLOAD;
- await getSyntheticImageScores(input, true, dispatch, type, imageFile);
+ await getSyntheticImageScores(urlInput, true, dispatch, type, imageFile);
};
+ useEffect(() => {
+ if (urlParam) {
+ setInput(urlParam);
+ handleSubmit(urlParam);
+ }
+ }, []);
+
return (
{
fileInputTypesAccepted={"image/*"}
handleCloseSelectedFile={handleClose}
preprocessLocalFile={preprocessImage}
+ isParentLoading={isLoading}
/>
@@ -278,9 +332,10 @@ const SyntheticImageDetection = () => {
{result && (
)}
diff --git a/src/components/NavItems/tools/SyntheticImageDetection/syntheticImageDetectionResults.jsx b/src/components/NavItems/tools/SyntheticImageDetection/syntheticImageDetectionResults.jsx
index 9913a59fb..3290dff11 100644
--- a/src/components/NavItems/tools/SyntheticImageDetection/syntheticImageDetectionResults.jsx
+++ b/src/components/NavItems/tools/SyntheticImageDetection/syntheticImageDetectionResults.jsx
@@ -3,6 +3,7 @@ import {
Alert,
Box,
Card,
+ CardContent,
CardHeader,
Grid,
IconButton,
@@ -13,59 +14,99 @@ import { i18nLoadNamespace } from "components/Shared/Languages/i18nLoadNamespace
import { useSelector } from "react-redux";
import { useTrackEvent } from "Hooks/useAnalytics";
import { getclientId } from "components/Shared/GoogleAnalytics/MatomoAnalytics";
-import GaugeChartResult from "components/Shared/GaugeChartResults/GaugeChartResult";
+import CustomAlertScore from "../../../Shared/CustomAlertScore";
+import GaugeChart from "react-gauge-chart";
+import Tooltip from "@mui/material/Tooltip";
+import { exportReactElementAsJpg } from "../../../Shared/Utils/htmlUtils";
+import GaugeChartModalExplanation from "../../../Shared/GaugeChartModalExplanation";
+import NddDatagrid from "./NddDatagrid";
+import {
+ DETECTION_THRESHOLDS,
+ getSyntheticImageDetectionAlgorithmFromApiName,
+ SyntheticImageDetectionAlgorithm,
+ syntheticImageDetectionAlgorithms,
+} from "./SyntheticImageDetectionAlgorithms";
+
+/**
+ * Returns the alert color code for the given percentage n
+ * @param n {number}
+ * @returns {"error" | "warning" | "success"}
+ */
+export const getAlertColor = (n) => {
+ if (n >= DETECTION_THRESHOLDS.THRESHOLD_3) {
+ return "error";
+ } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_2) {
+ return "warning";
+ } else {
+ return "success";
+ }
+};
+
+export const getAlertLabel = (n, keyword) => {
+ if (n >= DETECTION_THRESHOLDS.THRESHOLD_3) {
+ return keyword("synthetic_image_detection_alert_label_4");
+ } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_2) {
+ return keyword("synthetic_image_detection_alert_label_3");
+ } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_1) {
+ return keyword("synthetic_image_detection_alert_label_2");
+ } else {
+ return keyword("synthetic_image_detection_alert_label_1");
+ }
+};
-const SyntheticImageDetectionResults = (props) => {
+export const getPercentageColorCode = (n) => {
+ if (n >= DETECTION_THRESHOLDS.THRESHOLD_3) {
+ return "#FF0000";
+ } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_2) {
+ return "#FFAA00";
+ } else {
+ return "green";
+ }
+};
+
+class NddResult {
+ /**
+ *
+ * @param id {number}
+ * @param image {string}
+ * @param archiveUrl {string}
+ * @param imageUrls {string}
+ * @param detectionResults {SyntheticImageDetectionAlgorithmResult[]}
+ */
+ constructor(id, image, archiveUrl, imageUrls, detectionResults) {
+ this.id = id;
+ this.image = image;
+ this.archiveUrl = archiveUrl;
+ this.imageUrls = imageUrls;
+ this.detectionResults = detectionResults;
+ }
+}
+
+const SyntheticImageDetectionResults = ({ results, url, handleClose, nd }) => {
const keyword = i18nLoadNamespace(
"components/NavItems/tools/SyntheticImageDetection",
);
- class SyntheticImageDetectionAlgorithmResult {
+ const role = useSelector((state) => state.userSession.user.roles);
+
+ class SyntheticImageDetectionAlgorithmResult extends SyntheticImageDetectionAlgorithm {
/**
*
- * @param methodName {string}
+ * @param syntheticImageDetectionAlgorithm {SyntheticImageDetectionAlgorithm}
* @param predictionScore {number}
* @param isError {boolean}
*/
- constructor(methodName, predictionScore, isError) {
- (this.methodName = methodName),
- (this.predictionScore = predictionScore),
- (this.isError = isError);
+ constructor(syntheticImageDetectionAlgorithm, predictionScore, isError) {
+ super(
+ syntheticImageDetectionAlgorithm.apiServiceName,
+ syntheticImageDetectionAlgorithm.name,
+ syntheticImageDetectionAlgorithm.description,
+ syntheticImageDetectionAlgorithm.roleNeeded,
+ );
+ (this.predictionScore = predictionScore), (this.isError = isError);
}
}
- const DeepfakeImageDetectionMethodNames = {
- gan: {
- name: keyword("synthetic_image_detection_gan_name"),
- description: keyword("synthetic_image_detection_gan_description"),
- },
- diffusion: {
- name: keyword("synthetic_image_detection_diffusion_name"),
- description: keyword("synthetic_image_detection_diffusion_description"),
- },
- progan_r50_grip: {
- name: keyword("synthetic_image_detection_progan_name"),
- description: keyword("synthetic_image_detection_progan_description"),
- },
- adm_r50_grip: {
- name: keyword("synthetic_image_detection_adm_name"),
- description: keyword("synthetic_image_detection_adm_description"),
- },
- progan_rine_mever: {
- name: keyword("synthetic_image_detection_progan_rine_mever_name"),
- description: keyword(
- "synthetic_image_detection_progan_rine_mever_description",
- ),
- },
- ldm_rine_mever: {
- name: keyword("synthetic_image_detection_ldm_rine_mever_name"),
- description: keyword(
- "synthetic_image_detection_ldm_rine_mever_description",
- ),
- },
- };
- const results = props.result;
- const url = props.url;
const imgElement = React.useRef(null);
const imgContainerRef = useRef(null);
@@ -80,68 +121,32 @@ const SyntheticImageDetectionResults = (props) => {
useEffect(() => {
setResultsHaveErrors(false);
+ let res = [];
- const diffusionScore = new SyntheticImageDetectionAlgorithmResult(
- //previously unina_report
- Object.keys(DeepfakeImageDetectionMethodNames)[1],
- !results.ldm_r50_grip_report.prediction
- ? 0
- : results.ldm_r50_grip_report.prediction * 100,
- !results.ldm_r50_grip_report.prediction,
- );
- const ganScore = new SyntheticImageDetectionAlgorithmResult(
- //previously gan_report
- Object.keys(DeepfakeImageDetectionMethodNames)[0],
- !results.gan_r50_mever_report.prediction
- ? 0
- : results.gan_r50_mever_report.prediction * 100,
- !results.gan_r50_mever_report.prediction,
- );
-
- const proganScore = new SyntheticImageDetectionAlgorithmResult(
- Object.keys(DeepfakeImageDetectionMethodNames)[2],
- !results.progan_r50_grip_report.prediction
- ? 0
- : results.progan_r50_grip_report.prediction * 100,
- !results.progan_r50_grip_report.prediction,
- );
-
- const admScore = new SyntheticImageDetectionAlgorithmResult(
- Object.keys(DeepfakeImageDetectionMethodNames)[3],
- !results.adm_r50_grip_report.prediction
- ? 0
- : results.adm_r50_grip_report.prediction * 100,
- !results.adm_r50_grip_report.prediction,
- );
+ for (const algorithm of syntheticImageDetectionAlgorithms) {
+ if (
+ !role.includes(algorithm.roleNeeded) &&
+ algorithm.roleNeeded.length > 0
+ ) {
+ continue;
+ }
- const proganRineScore = new SyntheticImageDetectionAlgorithmResult(
- Object.keys(DeepfakeImageDetectionMethodNames)[4],
- !results.progan_rine_mever_report.prediction
- ? 0
- : results.progan_rine_mever_report.prediction * 100,
- !results.progan_rine_mever_report.prediction,
- );
+ const algorithmReport = results[algorithm.apiServiceName + "_report"];
- const ldmRineScore = new SyntheticImageDetectionAlgorithmResult(
- Object.keys(DeepfakeImageDetectionMethodNames)[5],
- !results.ldm_rine_mever_report.prediction
- ? 0
- : results.ldm_rine_mever_report.prediction * 100,
- !results.ldm_rine_mever_report.prediction,
- );
+ if (algorithmReport) {
+ res.push(
+ new SyntheticImageDetectionAlgorithmResult(
+ algorithm,
+ !algorithmReport.prediction ? 0 : algorithmReport.prediction * 100,
+ algorithmReport.prediction === undefined,
+ ),
+ );
+ }
+ }
- const res = (
- role.includes("EXTRA_FEATURE")
- ? [
- diffusionScore,
- ganScore,
- proganScore,
- admScore,
- proganRineScore,
- ldmRineScore,
- ]
- : [diffusionScore, ganScore, proganScore]
- ).sort((a, b) => b.predictionScore - a.predictionScore);
+ res = res
+ .filter((i) => i !== undefined)
+ .sort((a, b) => b.predictionScore - a.predictionScore);
const hasResultError = () => {
for (const algorithm of res) {
@@ -166,7 +171,6 @@ const SyntheticImageDetectionResults = (props) => {
const client_id = getclientId();
const session = useSelector((state) => state.userSession);
- const role = useSelector((state) => state.userSession.user.roles);
const uid = session && session.user ? session.user.id : null;
useTrackEvent(
@@ -179,52 +183,6 @@ const SyntheticImageDetectionResults = (props) => {
uid,
);
- const handleClose = () => {
- props.handleClose();
- };
- const DETECTION_THRESHOLDS = {
- THRESHOLD_1: 50,
- THRESHOLD_2: 70,
- THRESHOLD_3: 90,
- };
-
- const getPercentageColorCode = (n) => {
- if (n >= DETECTION_THRESHOLDS.THRESHOLD_3) {
- return "#FF0000";
- } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_2) {
- return "#FFAA00";
- } else {
- return "green";
- }
- };
-
- /**
- * Returns the alert color code for the given percentage n
- * @param n {number}
- * @returns {"error" | "warning" | "success"}
- */
- const getAlertColor = (n) => {
- if (n >= DETECTION_THRESHOLDS.THRESHOLD_3) {
- return "error";
- } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_2) {
- return "warning";
- } else {
- return "success";
- }
- };
-
- const getAlertLabel = (n) => {
- if (n >= DETECTION_THRESHOLDS.THRESHOLD_3) {
- return keyword("synthetic_image_detection_alert_label_4");
- } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_2) {
- return keyword("synthetic_image_detection_alert_label_3");
- } else if (n >= DETECTION_THRESHOLDS.THRESHOLD_1) {
- return keyword("synthetic_image_detection_alert_label_2");
- } else {
- return keyword("synthetic_image_detection_alert_label_1");
- }
- };
-
/**
* Returns a percentage between 0 and 99 for display purposes. We exclude 0 and 100 values.
* @param percentage {number}
@@ -245,6 +203,20 @@ const SyntheticImageDetectionResults = (props) => {
);
};
+ const [nddDetailsPanelMessage, setNddDetailsPanelMessage] = useState(
+ "synthetic_image_detection_ndd_additional_results_hide",
+ );
+ const handleNddDetailsChange = () => {
+ nddDetailsPanelMessage ===
+ "synthetic_image_detection_ndd_additional_results_hide"
+ ? setNddDetailsPanelMessage(
+ "synthetic_image_detection_ndd_additional_results",
+ )
+ : setNddDetailsPanelMessage(
+ "synthetic_image_detection_ndd_additional_results_hide",
+ );
+ };
+
const keywords = [
"gauge_scale_modal_explanation_rating_1",
"gauge_scale_modal_explanation_rating_2",
@@ -253,65 +225,343 @@ const SyntheticImageDetectionResults = (props) => {
];
const colors = ["#00FF00", "#AAFF03", "#FFA903", "#FF0000"];
+ /**
+ *
+ * @param nddResults
+ * @returns {NddResult[]}
+ */
+ const getNddRows = (nddResults) => {
+ let rows = [];
+ for (let i = 0; i < nddResults.length; i += 1) {
+ const res = nddResults[i];
+ let detectionResults = [];
+ for (const detection of Object.keys(res.detections)) {
+ const d = new SyntheticImageDetectionAlgorithmResult(
+ getSyntheticImageDetectionAlgorithmFromApiName(detection),
+ sanitizeDetectionPercentage(res.detections[detection] * 100),
+ false,
+ );
+
+ // Display iff the user has the permissions to see the content
+ if (role.includes(d.roleNeeded) || !d.roleNeeded)
+ detectionResults.push(d);
+ }
+
+ if (detectionResults.length === 0) {
+ continue;
+ }
+
+ rows.push(
+ new NddResult(
+ i + 1,
+ res.archive_url,
+ res.archive_url,
+ res.origin_urls,
+ detectionResults,
+ ),
+ );
+ }
+ return rows;
+ };
+
return (
-
-
-
-
-
- }
- />
+
+
+
+
+ }
+ />
+
-
-
-
-
+
+
+
+ >
+
+
+
+
+
+ {nd && nd.similar_media && nd.similar_media.length > 0 && (
+
+
+
+ {keyword("synthetic_image_detection_ndd_info")}
+
+
-
+ )}
{syntheticImageScores.length > 0 ? (
-
+
+
+
+
+ {maxScore > DETECTION_THRESHOLDS.THRESHOLD_2 && (
+
+ {keyword(
+ "synthetic_image_detection_generic_detection_text",
+ )}
+
+ )}
+
+
+
+
+
+ {keyword(
+ "synthetic_image_detection_gauge_no_detection",
+ )}
+
+
+ {keyword("synthetic_image_detection_gauge_detection")}
+
+
+
+
+
+
+
+ await exportReactElementAsJpg(
+ gaugeChartRef,
+ "gauge_chart",
+ )
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ {resultsHaveErrors && (
+
+ {keyword("synthetic_image_detection_algorithms_errors")}
+
+ )}
+
+
+ }>
+ {keyword(detailsPanelMessage)}
+
+
+
+ {syntheticImageScores.map((item, key) => {
+ let predictionScore;
+
+ if (item.predictionScore) {
+ predictionScore = sanitizeDetectionPercentage(
+ item.predictionScore,
+ );
+ }
+
+ return (
+
+
+
+
+
+ {keyword(item.name)}
+
+
+
+ {item.isError ? (
+
+ {keyword(
+ "synthetic_image_detection_error_generic",
+ )}
+
+ ) : (
+ <>
+
+ {keyword(
+ "synthetic_image_detection_probability_text",
+ )}{" "}
+
+
+ {predictionScore}%
+
+ >
+ )}
+
+ {!item.isError && (
+
+ )}
+
+
+
+
+
+
+ {keyword(item.description)}
+
+
+
+ {syntheticImageScores.length > key + 1 && (
+
+ )}
+
+ );
+ })}
+
+
+
+
+
) : (
{
)}
+
+ {nd && nd.similar_media && nd.similar_media.length > 0 && (
+
+
+ }>
+ {keyword(nddDetailsPanelMessage)}
+
+
+
+
+
+
+
+
+ )}
+
-
-
+
+
);
};
diff --git a/src/components/Shared/StringFileUploadField/index.jsx b/src/components/Shared/StringFileUploadField/index.jsx
index a365539df..a3cf40afd 100644
--- a/src/components/Shared/StringFileUploadField/index.jsx
+++ b/src/components/Shared/StringFileUploadField/index.jsx
@@ -1,8 +1,9 @@
-import React, { useRef, useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
import Box from "@mui/material/Box";
import { Button, ButtonGroup, Grid, TextField } from "@mui/material";
import FolderOpenIcon from "@mui/icons-material/FolderOpen";
import CloseIcon from "@mui/icons-material/Close";
+import LoadingButton from "@mui/lab/LoadingButton";
/**
* A reusable form component with a textfield and a local file with optional processing
@@ -20,7 +21,7 @@ import CloseIcon from "@mui/icons-material/Close";
* @param handleSubmit {any}
* @param handleCloseSelectedFile {any} An optional handler function to execute when clearing the file selected
* @param preprocessLocalFile {any} Optional preprocessing function to process a local file
-
+ * @param isParentLoading {?Boolean | undefined} Optional boolean to change the loading state of the component from a parent component
*/
const StringFileUploadField = ({
@@ -36,10 +37,19 @@ const StringFileUploadField = ({
fileInputTypesAccepted,
handleCloseSelectedFile,
preprocessLocalFile,
+ isParentLoading,
}) => {
const fileRef = useRef(null);
- const [isLoading, setIsLoading] = useState(false);
+ const [isLoading, setIsLoading] = useState(
+ isParentLoading !== undefined ? isParentLoading : false,
+ );
+
+ useEffect(() => {
+ if (isParentLoading !== undefined && isLoading !== isParentLoading) {
+ setIsLoading(isParentLoading);
+ }
+ }, [isParentLoading]);
return (
@@ -58,7 +68,7 @@ const StringFileUploadField = ({
/>
-
+
diff --git a/src/components/SideMenu/index.jsx b/src/components/SideMenu/index.jsx
index 21b31187d..5dc2b8967 100644
--- a/src/components/SideMenu/index.jsx
+++ b/src/components/SideMenu/index.jsx
@@ -31,11 +31,11 @@ import ImageIcon from "../NavBar/images/SVG/Image/Images.svg";
import SearchIcon from "../NavBar/images/SVG/Search/Search.svg";
import DataIcon from "../NavBar/images/SVG/DataAnalysis/Data_analysis.svg";
import {
- ROLES,
TOOL_GROUPS,
TOOL_STATUS_ICON,
TOOLS_CATEGORIES,
} from "../../constants/tools";
+import { ROLES } from "../../constants/roles";
import { selectTopMenuItem } from "../../redux/reducers/navReducer";
import { TOP_MENU_ITEMS } from "../../constants/topMenuItems";
import { selectTool } from "../../redux/reducers/tools/toolReducer";
diff --git a/src/constants/roles.jsx b/src/constants/roles.jsx
new file mode 100644
index 000000000..c3dc42f18
--- /dev/null
+++ b/src/constants/roles.jsx
@@ -0,0 +1,11 @@
+/**
+ * Represents the user roles that can be needed to access a given topMenuItem
+ * @typedef Roles
+ * @type {{BETA_TESTER: string, ARCHIVE: string, LOCK: string}}
+ */
+export const ROLES = {
+ ARCHIVE: "ARCHIVE",
+ BETA_TESTER: "BETA_TESTER",
+ LOCK: "lock",
+ EXTRA_FEATURE: "EXTRA_FEATURE",
+};
diff --git a/src/constants/tools.jsx b/src/constants/tools.jsx
index 19d6ad90f..3a3a6edaa 100644
--- a/src/constants/tools.jsx
+++ b/src/constants/tools.jsx
@@ -53,6 +53,7 @@ import SemanticSearch from "../components/NavItems/tools/SemanticSearch";
import TwitterSna from "../components/NavItems/tools/TwitterSna/TwitterSna";
import Archive from "../components/NavItems/tools/Archive";
import About from "../components/NavItems/About/About";
+import { ROLES } from "./roles";
/**
* Represents the categories to which the tools belong
@@ -99,17 +100,6 @@ export const TOOL_GROUPS = {
MORE: "more",
};
-/**
- * Represents the user roles that can be needed to access a given topMenuItem
- * @typedef Roles
- * @type {{BETA_TESTER: string, ARCHIVE: string, LOCK: string}}
- */
-export const ROLES = {
- ARCHIVE: "ARCHIVE",
- BETA_TESTER: "BETA_TESTER",
- LOCK: "lock",
-};
-
/**
* Represents a topMenuItem that can be used by users
*/
@@ -425,7 +415,7 @@ const imageGif = new Tool(
,
);
-const imageSyntheticDetection = new Tool(
+export const imageSyntheticDetection = new Tool(
"navbar_synthetic_image_detection",
"navbar_synthetic_image_detection_description",
syntheticImageSvgIcon,
diff --git a/src/i18n.jsx b/src/i18n.jsx
index 84bd9c2ff..aaf958472 100644
--- a/src/i18n.jsx
+++ b/src/i18n.jsx
@@ -18,6 +18,9 @@ i18next
interpolation: {
escapeValue: false,
},
+ // react: {
+ // useSuspense: false,
+ // },
load: "languageOnly",
backend: {
backends: [
diff --git a/src/redux/actions/tools/syntheticImageDetectionActions.jsx b/src/redux/actions/tools/syntheticImageDetectionActions.jsx
index acb25a804..dd38de66a 100644
--- a/src/redux/actions/tools/syntheticImageDetectionActions.jsx
+++ b/src/redux/actions/tools/syntheticImageDetectionActions.jsx
@@ -11,3 +11,7 @@ export const setSyntheticImageDetectionLoading = createAction(
export const setSyntheticImageDetectionResult = createAction(
"SET_SYNTHETIC_IMAGE_DETECTION_RESULT",
);
+
+export const setSyntheticImageDetectionNearDuplicates = createAction(
+ "SET_SYNTHETIC_IMAGE_DETECTION_NEAR_DUPLICATES",
+);
diff --git a/src/redux/reducers/tools/syntheticImageDetectionReducer.jsx b/src/redux/reducers/tools/syntheticImageDetectionReducer.jsx
index 881bb8107..2ceb64169 100644
--- a/src/redux/reducers/tools/syntheticImageDetectionReducer.jsx
+++ b/src/redux/reducers/tools/syntheticImageDetectionReducer.jsx
@@ -2,16 +2,14 @@ const defaultState = {
url: "",
result: null,
loading: false,
+ duplicates: null,
};
const syntheticImageDetectionReducer = (state = defaultState, action) => {
switch (action.type) {
case "SYNTHETIC_IMAGE_DETECTION_RESET":
return {
- ...state,
- url: "",
- result: null,
- loading: false,
+ ...defaultState,
};
case "SET_SYNTHETIC_IMAGE_DETECTION_LOADING":
return {
@@ -25,6 +23,12 @@ const syntheticImageDetectionReducer = (state = defaultState, action) => {
result: action.payload.result,
loading: false,
};
+ case "SET_SYNTHETIC_IMAGE_DETECTION_NEAR_DUPLICATES":
+ return {
+ ...state,
+ duplicates: action.payload,
+ loading: false,
+ };
default:
return state;
}