v0.3.2
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import type {
|
||||
ImageCandidate,
|
||||
PageCandidate,
|
||||
PageDisclosureCandidate,
|
||||
TextCandidate
|
||||
} from '../shared/types';
|
||||
import { discoverHeuristics } from './heuristics';
|
||||
|
||||
const IMAGE_ID_ATTRIBUTE = 'data-slopdetect-image-id';
|
||||
const MAX_TEXT_BLOCKS = 8;
|
||||
const MIN_TEXT_CHARACTERS = 300;
|
||||
|
||||
function isVisible(element: Element): boolean {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(element);
|
||||
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
|
||||
}
|
||||
|
||||
function makeImageCandidate(element: Element, url: string, alt = ''): ImageCandidate | undefined {
|
||||
if (!url || !isVisible(element)) return undefined;
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width < 16 || rect.height < 16) return undefined;
|
||||
const existingId = element.getAttribute(IMAGE_ID_ATTRIBUTE);
|
||||
const id = existingId || crypto.randomUUID();
|
||||
element.setAttribute(IMAGE_ID_ATTRIBUTE, id);
|
||||
return {
|
||||
id,
|
||||
url: new URL(url, document.baseURI).href,
|
||||
alt: alt.trim().slice(0, 200),
|
||||
width: Math.round(rect.width),
|
||||
height: Math.round(rect.height)
|
||||
};
|
||||
}
|
||||
|
||||
function discoverImages(): ImageCandidate[] {
|
||||
const candidates: ImageCandidate[] = [];
|
||||
const seenUrls = new Set<string>();
|
||||
|
||||
for (const image of document.querySelectorAll('img')) {
|
||||
const candidate = makeImageCandidate(image, image.currentSrc || image.src, image.alt);
|
||||
if (candidate && !seenUrls.has(candidate.url)) {
|
||||
candidates.push(candidate);
|
||||
seenUrls.add(candidate.url);
|
||||
}
|
||||
}
|
||||
|
||||
for (const image of document.querySelectorAll('svg image')) {
|
||||
const href = image.getAttribute('href') || image.getAttribute('xlink:href') || '';
|
||||
const candidate = makeImageCandidate(image, href, image.getAttribute('aria-label') || 'SVG image');
|
||||
if (candidate && !seenUrls.has(candidate.url)) {
|
||||
candidates.push(candidate);
|
||||
seenUrls.add(candidate.url);
|
||||
}
|
||||
}
|
||||
|
||||
const elements = [...document.querySelectorAll('body *')].slice(0, 2000);
|
||||
for (const element of elements) {
|
||||
const background = window.getComputedStyle(element).backgroundImage;
|
||||
const match = /^url\(["']?(.*?)["']?\)$/.exec(background);
|
||||
if (!match?.[1]) continue;
|
||||
const candidate = makeImageCandidate(element, match[1], element.getAttribute('aria-label') || 'Background image');
|
||||
if (candidate && !seenUrls.has(candidate.url)) {
|
||||
candidates.push(candidate);
|
||||
seenUrls.add(candidate.url);
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export function estimateTokens(text: string): number {
|
||||
return text.trim() ? Math.ceil(text.trim().split(/\s+/).length * 1.3) : 0;
|
||||
}
|
||||
|
||||
function discoverText(): TextCandidate[] {
|
||||
const selectors = 'article p, main p, [role="main"] p, article li, main li, blockquote';
|
||||
const values = [...document.querySelectorAll<HTMLElement>(selectors)]
|
||||
.filter(isVisible)
|
||||
.map((element) => element.innerText.trim())
|
||||
.filter((text) => text.length >= MIN_TEXT_CHARACTERS);
|
||||
|
||||
const unique = [...new Set(values)].sort((left, right) => right.length - left.length).slice(0, MAX_TEXT_BLOCKS);
|
||||
if (unique.length === 0) {
|
||||
const root = document.querySelector<HTMLElement>('article, main, [role="main"]') || document.body;
|
||||
const combined = root?.innerText.trim().slice(0, 24_000) || '';
|
||||
if (combined.length >= MIN_TEXT_CHARACTERS) unique.push(combined);
|
||||
}
|
||||
|
||||
return unique.map((text) => ({ id: crypto.randomUUID(), text, tokenEstimate: estimateTokens(text) }));
|
||||
}
|
||||
|
||||
function pushDisclosure(
|
||||
output: PageDisclosureCandidate[],
|
||||
source: string,
|
||||
value: string | null | undefined
|
||||
): void {
|
||||
const trimmed = value?.trim();
|
||||
if (trimmed) output.push({ source, value: trimmed.slice(0, 2000) });
|
||||
}
|
||||
|
||||
function discoverDisclosures(): PageDisclosureCandidate[] {
|
||||
const candidates: PageDisclosureCandidate[] = [];
|
||||
const interestingMeta = /ai|artificial|synthetic|provenance|credential|generator|content-origin/i;
|
||||
|
||||
for (const meta of document.querySelectorAll<HTMLMetaElement>('meta[name], meta[property]')) {
|
||||
const name = meta.name || meta.getAttribute('property') || '';
|
||||
if (interestingMeta.test(name)) {
|
||||
pushDisclosure(candidates, `meta:${name}`, meta.content);
|
||||
}
|
||||
}
|
||||
|
||||
for (const link of document.querySelectorAll<HTMLLinkElement>('link[rel]')) {
|
||||
if (/credential|provenance|c2pa|manifest/i.test(link.rel)) {
|
||||
pushDisclosure(candidates, `link:${link.rel}`, link.href);
|
||||
}
|
||||
}
|
||||
|
||||
for (const script of document.querySelectorAll<HTMLScriptElement>('script[type="application/ld+json"]')) {
|
||||
if (/"(?:aiGenerated|isAIGenerated|contentOrigin|digitalSourceType)"\s*:\s*(?:true|"[^"]*(?:trainedAlgorithmicMedia|synthetic|AI)[^"]*")/i.test(script.textContent || '')) {
|
||||
pushDisclosure(candidates, 'structured-data', script.textContent);
|
||||
}
|
||||
}
|
||||
|
||||
const labelSelectors = 'figcaption, small, [class*="disclosure" i], [class*="provenance" i], [class*="credit" i], [aria-label*="AI-generated" i]';
|
||||
for (const element of document.querySelectorAll<HTMLElement>(labelSelectors)) {
|
||||
const value = (element.innerText || element.getAttribute('aria-label') || '').trim();
|
||||
if (value.length <= 300 && /\b(?:AI[- ]generated|generated by AI|artificially generated|created with AI|synthetic media)\b/i.test(value)) {
|
||||
pushDisclosure(candidates, 'visible-label', value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const element of document.querySelectorAll<HTMLElement>('[data-ai-generated], [data-content-origin]')) {
|
||||
pushDisclosure(
|
||||
candidates,
|
||||
element.hasAttribute('data-ai-generated') ? 'data-ai-generated' : 'data-content-origin',
|
||||
element.getAttribute('data-ai-generated') || element.getAttribute('data-content-origin')
|
||||
);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export function discoverPage(): PageCandidate {
|
||||
return {
|
||||
url: location.href,
|
||||
title: document.title,
|
||||
images: discoverImages(),
|
||||
texts: discoverText(),
|
||||
disclosures: discoverDisclosures(),
|
||||
heuristics: discoverHeuristics()
|
||||
};
|
||||
}
|
||||
|
||||
export { IMAGE_ID_ATTRIBUTE };
|
||||
Reference in New Issue
Block a user