init
This commit is contained in:
commit
f2b8bdca98
33 changed files with 2068 additions and 0 deletions
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
wasm/target
|
||||||
|
wasm/Cargo.lock
|
||||||
|
wasm/inference-models/models
|
||||||
|
wasm/inference-models/pkg
|
||||||
|
extension/.output
|
||||||
|
extension/.wxt
|
||||||
|
extension/node_modules
|
||||||
|
extension/pnpm-lock.yaml
|
||||||
|
.export_venv
|
||||||
127
Makefile.toml
Normal file
127
Makefile.toml
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
[env.development]
|
||||||
|
_MK_WASMPACK_MOD = "--dev"
|
||||||
|
|
||||||
|
[env.production]
|
||||||
|
_MK_WASMPACK_MOD = "--release"
|
||||||
|
|
||||||
|
[tasks.release]
|
||||||
|
dependencies = [
|
||||||
|
"build_inference_models",
|
||||||
|
"build_extension"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.dev]
|
||||||
|
dependencies = [
|
||||||
|
"build_inference_models",
|
||||||
|
"extension_npm_setup"
|
||||||
|
]
|
||||||
|
cwd = "extension"
|
||||||
|
command = "pnpm"
|
||||||
|
args = ["dev"]
|
||||||
|
|
||||||
|
[tasks.build_extension]
|
||||||
|
dependencies = [
|
||||||
|
"build_extension_mozf",
|
||||||
|
"zip_extension_mozf",
|
||||||
|
"build_extension_chro",
|
||||||
|
"zip_extension_chro"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.zip_extension_chro]
|
||||||
|
cwd = "extension"
|
||||||
|
command = "pnpm"
|
||||||
|
args = ["zip:chrome"]
|
||||||
|
|
||||||
|
[tasks.zip_extension_mozf]
|
||||||
|
cwd = "extension"
|
||||||
|
command = "pnpm"
|
||||||
|
args = ["zip:firefox"]
|
||||||
|
|
||||||
|
[tasks.build_extension_chro]
|
||||||
|
dependencies = [
|
||||||
|
"extension_npm_setup"
|
||||||
|
]
|
||||||
|
cwd = "extension"
|
||||||
|
command = "pnpm"
|
||||||
|
args = ["build:chrome"]
|
||||||
|
|
||||||
|
[tasks.build_extension_mozf]
|
||||||
|
dependencies = [
|
||||||
|
"extension_npm_setup"
|
||||||
|
]
|
||||||
|
cwd = "extension"
|
||||||
|
command = "pnpm"
|
||||||
|
args = ["build:firefox"]
|
||||||
|
|
||||||
|
[tasks.extension_npm_setup]
|
||||||
|
cwd = "extension"
|
||||||
|
command = "pnpm"
|
||||||
|
args = ["install"]
|
||||||
|
|
||||||
|
[tasks.build_inference_models]
|
||||||
|
dependencies = [
|
||||||
|
"build_inference_models_ai_text",
|
||||||
|
"build_inference_models_ai_text_lite"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.build_inference_models_ai_text]
|
||||||
|
env = {_MK_MODEL_AI_TEXT = "onnx-community/modernbert-ai-detection-raid-mage-ONNX",_MK_MODEL_AI_TEXT_ATTENTION = "yes",_MK_MODEL_AI_TEXT_TYPES = "no"}
|
||||||
|
cwd = "wasm/inference-models"
|
||||||
|
install_crate = "wasm-pack"
|
||||||
|
command = "wasm-pack"
|
||||||
|
args = [
|
||||||
|
"build",
|
||||||
|
"${_MK_WASMPACK_MOD}",
|
||||||
|
"--reference-types",
|
||||||
|
"--target", "web",
|
||||||
|
"--out-dir", "pkg/inference-ai-text",
|
||||||
|
"--out-name", "inference-ai-text",
|
||||||
|
"--features", "ai_text"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.build_inference_models_ai_text_lite]
|
||||||
|
env = {_MK_MODEL_AI_TEXT = "onnx-community/e5-small-lora-ai-generated-detector-ONNX",_MK_MODEL_AI_TEXT_ATTENTION = "yes",_MK_MODEL_AI_TEXT_TYPES = "yes"}
|
||||||
|
cwd = "wasm/inference-models"
|
||||||
|
install_crate = "wasm-pack"
|
||||||
|
command = "wasm-pack"
|
||||||
|
args = [
|
||||||
|
"build",
|
||||||
|
"${_MK_WASMPACK_MOD}",
|
||||||
|
"--reference-types",
|
||||||
|
"--target", "web",
|
||||||
|
"--out-dir", "pkg/inference-ai-text-lite",
|
||||||
|
"--out-name", "inference-ai-text-lite",
|
||||||
|
"--features", "ai_text"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.export_onnx]
|
||||||
|
dependencies = ["export_onnx_ai_text"]
|
||||||
|
|
||||||
|
[tasks.export_onnx_ai_text]
|
||||||
|
dependencies = ["exportenv"]
|
||||||
|
command = ".export_venv/bin/optimum-cli"
|
||||||
|
args = [
|
||||||
|
"export", "onnx",
|
||||||
|
"--model", "GeorgeDrayson/modernbert-ai-detection-raid-mage",
|
||||||
|
"--task", "text-classification",
|
||||||
|
"--dtype", "fp32",
|
||||||
|
"--opset", "18",
|
||||||
|
"--optimize", "O1",
|
||||||
|
"--monolith",
|
||||||
|
"--no-post-process",
|
||||||
|
"wasm/inference-models/models/ai_text"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.exportenv]
|
||||||
|
dependencies = [
|
||||||
|
"exportenv_create",
|
||||||
|
"exportenv_setup"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.exportenv_create]
|
||||||
|
command = "python3"
|
||||||
|
args = ["-m", "venv", ".export_venv"]
|
||||||
|
|
||||||
|
[tasks.exportenv_setup]
|
||||||
|
command = ".export_venv/bin/pip"
|
||||||
|
args = ["install", "--upgrade", "-r", "exportenv_requirements.txt"]
|
||||||
68
README.md
Normal file
68
README.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# firaid-moz
|
||||||
|
> I don't know why I named it this way
|
||||||
|
|
||||||
|
A simple local webextension, which can run a bunch of small ML models to infer some properties of DOM content, such as:
|
||||||
|
|
||||||
|
- Probability of text being AI/Human created
|
||||||
|
- Probability of image being AI/Human created
|
||||||
|
|
||||||
|
## AI/Human classification
|
||||||
|
|
||||||
|
> **NONE** of ML/AI/whatever models is 100% accurate by the very fundamental design. Any ML model *pretends* to do some job with certain degree of accuracy. So this extension *pretends* to know which content is AI-generated and which is not - with a certain degree of accuracy.
|
||||||
|
>
|
||||||
|
>If you don't completely trust the "AI slop" - you should not completely trust this extension also!
|
||||||
|
>
|
||||||
|
> False-positives? - Possible
|
||||||
|
>
|
||||||
|
> False-negatives? - Probably
|
||||||
|
>
|
||||||
|
> This extension - like **every other AI** - is a **tool** which aims to help you and **not** replace your own discretion.
|
||||||
|
|
||||||
|
No, I do not hate the AI. It's cool. I hate the *slop* - when the entire "technical guide" article is nothing more then a bunch of AI ejacuation. But how do I know that slop is a slop?
|
||||||
|
|
||||||
|
Yeah.. I have to read it. And I had _enough_ of reading. So I want to see a warning badge like "its AI" - to help me navigate through text/image content.
|
||||||
|
|
||||||
|
AI/Human classification can be done in four different modes (sorted in order of preference for the potato hardware):
|
||||||
|
|
||||||
|
0. _(potato)_: No classification
|
||||||
|
1. _(normal)_: Lightweight model, which can have "no decision at all" in some cases
|
||||||
|
2. _(high)_: Lightweight model still does its job, but when its decision is weak - heavy model takes the task
|
||||||
|
3. _(ultra)_: Heavy and more accurate model performs always, lightweight model is disabled
|
||||||
|
|
||||||
|
The _normal_ mode requires ~150Mb of RAM for text classification and ~200Mb of RAM for image classification.
|
||||||
|
|
||||||
|
The _ultra_ mode requires ~700Mb of RAM for text classification and ~500Mb of RAM for image classification.
|
||||||
|
|
||||||
|
## `Microslop` mode
|
||||||
|
|
||||||
|
> Slop-sloppity slop, micro slop slop slop, microslop microslop microslop
|
||||||
|
|
||||||
|
That's easy. Turn is on and every time the page contains word "microsoft" (except for the URLs) - this extention will replace it with "microslop"
|
||||||
|
|
||||||
|
## `Githief` mode
|
||||||
|
|
||||||
|
You know what they do with your "licensed" and "private repository" code.
|
||||||
|
|
||||||
|
Turn this on - and all "github" (except for urls) shall turn into "githief"
|
||||||
|
|
||||||
|
## `Хуяндекс` mode
|
||||||
|
|
||||||
|
At 2025 the "yandex" corporation was caught in bypassing the data permissions on Android smartphones to collect and possibly provide all kinds of your personal information to the third-parties; to track you; to deanon you; thus violating your consent and privacy.
|
||||||
|
|
||||||
|
You can read more about it [here](https://localmess.github.io/)
|
||||||
|
|
||||||
|
They stopped that practice after disclosure, yes, but they did it nontheless.
|
||||||
|
|
||||||
|
So you can turn on this feature and every occurence of word "яндекс" shall be replaced for "хуяндекс" (russian wordplay, something like "fuck-yandex") and every occurence of word "yandex" (except for urls) shall be replaced with your custom word (I did not figure out proper wordplay for it yet).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A bit of technical details
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Wooow, I am so very glad thay you're reading this section! Thank you in advance for interest to contributing.
|
||||||
|
|
||||||
|
As you can see - this is not a github, but there is a [mirror](https://github.com/yiffyrusdev/spushks-firaid-moz), which my friend was so kind to provide. You can open issues and make PRs there - we'll merge them, preserving your identity.
|
||||||
|
|
||||||
|
Also, if you sumbit any code, it is considered Apache-2.0
|
||||||
2
exportenv_requirements.txt
Normal file
2
exportenv_requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
optimum-onnx[onnxruntime]
|
||||||
|
accelerate
|
||||||
15
extension/modules/inference-models.ts
Normal file
15
extension/modules/inference-models.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { defineWxtModule } from "wxt/modules";
|
||||||
|
|
||||||
|
export default defineWxtModule((wxt) => {
|
||||||
|
wxt.hook("build:publicAssets", (_, assets) => {
|
||||||
|
assets.push({
|
||||||
|
absoluteSrc: resolve("node_modules/inference-ai-text/inference-ai-text_bg.wasm"),
|
||||||
|
relativeDest: "inference-ai-text_bg.wasm",
|
||||||
|
});
|
||||||
|
assets.push({
|
||||||
|
absoluteSrc: resolve("node_modules/inference-ai-text-lite/inference-ai-text-lite_bg.wasm"),
|
||||||
|
relativeDest: "inference-ai-text-lite_bg.wasm",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
37
extension/package.json
Normal file
37
extension/package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"name": "firaid-moz",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "This extension will search and highlight AI-generated text and images with small badges while you surfing. Our keen and friendly nose shall give you a heads-up, so you can dodge the slop 🦊. By the way, none data will ever leave your browser - this extension works absolutely locally. We also have cool features like 'Microslop-mode' and 'PII protection'",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "wxt",
|
||||||
|
"dev:firefox": "wxt -b firefox",
|
||||||
|
"build:chrome": "wxt build --mv3",
|
||||||
|
"build:firefox": "wxt build -b firefox --mv3",
|
||||||
|
"zip:chrome": "wxt zip --mv3",
|
||||||
|
"zip:firefox": "wxt zip -b firefox --mv3",
|
||||||
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"postinstall": "wxt prepare"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Svyatoslav Pushkarev <spushkarevwork@yandex.ru>",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"packageManager": "pnpm@10.28.2",
|
||||||
|
"dependencies": {
|
||||||
|
"@webext-core/messaging": "^2.3.0",
|
||||||
|
"inference-ai-text": "file:../wasm/inference-models/pkg/inference-ai-text",
|
||||||
|
"inference-ai-text-lite": "file:../wasm/inference-models/pkg/inference-ai-text-lite"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"@tsconfig/svelte": "^5.0.7",
|
||||||
|
"@wxt-dev/module-svelte": "^2.0.4",
|
||||||
|
"daisyui": "^5.5.19",
|
||||||
|
"svelte": "^5.50.0",
|
||||||
|
"svelte-check": "^4.3.6",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"wxt": "^0.20.14"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
extension/src/app.config.ts
Normal file
7
extension/src/app.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineAppConfig } from "#imports";
|
||||||
|
|
||||||
|
declare module "wxt/utils/define-app-config" {
|
||||||
|
export interface WxtAppConfig {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineAppConfig({});
|
||||||
BIN
extension/src/assets/favicon.png
Normal file
BIN
extension/src/assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
156
extension/src/entrypoints/background.ts
Normal file
156
extension/src/entrypoints/background.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
import inference_ai_text, { AiText, Capability as AiTextCapability } from "inference-ai-text";
|
||||||
|
import inference_ai_text_lite, {
|
||||||
|
AiText as AiTextLite,
|
||||||
|
Capability as AiTextLiteCapability,
|
||||||
|
} from "inference-ai-text-lite";
|
||||||
|
|
||||||
|
import { onMessage } from "@/utils/messaging";
|
||||||
|
import { ExtLogLevel } from "@/utils/logging";
|
||||||
|
import { ExtensionConfigAiText } from "@/utils/config";
|
||||||
|
|
||||||
|
export default defineBackground({
|
||||||
|
type: "module",
|
||||||
|
main() {
|
||||||
|
setup_inference_ai_text();
|
||||||
|
setup_ext_console();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function setup_ext_console() {
|
||||||
|
onMessage("log", ({ data }) => {
|
||||||
|
switch (data.level) {
|
||||||
|
case ExtLogLevel.Debug:
|
||||||
|
console.debug(`[${data.sender}]`, ...data.args);
|
||||||
|
break;
|
||||||
|
case ExtLogLevel.Info:
|
||||||
|
console.log(`[${data.sender}]`, ...data.args);
|
||||||
|
break;
|
||||||
|
case ExtLogLevel.Warning:
|
||||||
|
console.warn(`[${data.sender}]`, ...data.args);
|
||||||
|
break;
|
||||||
|
case ExtLogLevel.Error:
|
||||||
|
console.error(`[${data.sender}]`, ...data.args);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const AI_TEXT_STATE: {
|
||||||
|
handle: AiText | null;
|
||||||
|
queue_inference_timer: number | null;
|
||||||
|
have_normal_model: boolean;
|
||||||
|
have_lite_model: boolean;
|
||||||
|
} = {
|
||||||
|
have_normal_model: false,
|
||||||
|
have_lite_model: false,
|
||||||
|
handle: null,
|
||||||
|
queue_inference_timer: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function setup_inference_ai_text() {
|
||||||
|
extensionConfig.ai_text.getValue().then((config) => {
|
||||||
|
configure_ai_text(config);
|
||||||
|
});
|
||||||
|
extensionConfig.ai_text.watch((config) => {
|
||||||
|
configure_ai_text(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMessage("inference_aiText_classify", ({ data: { text } }) => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (!AI_TEXT_STATE.handle) {
|
||||||
|
reject({
|
||||||
|
reason: "TextAi inference is disabled",
|
||||||
|
errors: [],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug("TEXT", text);
|
||||||
|
|
||||||
|
AI_TEXT_STATE.handle
|
||||||
|
.queue(text)
|
||||||
|
.then((r) => {
|
||||||
|
// we're passing that into outer space, can not play with reference
|
||||||
|
resolve({ prob_ai: r.prob_ai, prob_human: r.prob_human });
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
reject({
|
||||||
|
reason: "inference error",
|
||||||
|
errors: [e],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run inference if reported queue size reached threshold,
|
||||||
|
// or schedule inference to run anyway after some time
|
||||||
|
let stats = AI_TEXT_STATE.handle.stats();
|
||||||
|
if (stats.queue_size > stats.queue_limit / 2) {
|
||||||
|
AI_TEXT_STATE.handle.run_queued_inference().catch((e) => {
|
||||||
|
console.error("Queue-threshold initiated inference failed", e);
|
||||||
|
});
|
||||||
|
if (AI_TEXT_STATE.queue_inference_timer) {
|
||||||
|
clearTimeout(AI_TEXT_STATE.queue_inference_timer);
|
||||||
|
}
|
||||||
|
AI_TEXT_STATE.queue_inference_timer = null;
|
||||||
|
} else if (!AI_TEXT_STATE.queue_inference_timer) {
|
||||||
|
AI_TEXT_STATE.queue_inference_timer = setTimeout(() => {
|
||||||
|
if (AI_TEXT_STATE.handle) {
|
||||||
|
AI_TEXT_STATE.queue_inference_timer = null;
|
||||||
|
AI_TEXT_STATE.handle.run_queued_inference().catch((e) => {
|
||||||
|
console.error("Timed initiated inference failed", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function configure_ai_text(config: ExtensionConfigAiText) {
|
||||||
|
if (config.enabled) {
|
||||||
|
const loader = config.lite ? inference_ai_text_lite : inference_ai_text;
|
||||||
|
const handler = config.lite ? AiTextLite : AiText;
|
||||||
|
const capability = config.lite ? AiTextLiteCapability : AiTextCapability;
|
||||||
|
const have = config.lite ? AI_TEXT_STATE.have_lite_model : AI_TEXT_STATE.have_normal_model;
|
||||||
|
const set_have = config.lite
|
||||||
|
? () => (AI_TEXT_STATE.have_lite_model = true)
|
||||||
|
: () => (AI_TEXT_STATE.have_normal_model = true);
|
||||||
|
|
||||||
|
if (!have) {
|
||||||
|
await loader();
|
||||||
|
if (await guess_has_webgpu()) {
|
||||||
|
console.warn(
|
||||||
|
"WebGPU detected, but use is not currently possible due to burn APIs being sync. Check the issue progress in burn and burn-onnx repos. In the mean time will use NdArray anyway.",
|
||||||
|
);
|
||||||
|
//await capability.assert_ndarray();
|
||||||
|
await capability.assert_webgpu();
|
||||||
|
} else {
|
||||||
|
await capability.assert_ndarray();
|
||||||
|
}
|
||||||
|
set_have();
|
||||||
|
}
|
||||||
|
|
||||||
|
AI_TEXT_STATE.handle = new handler();
|
||||||
|
} else {
|
||||||
|
AI_TEXT_STATE.handle = null;
|
||||||
|
if (AI_TEXT_STATE.queue_inference_timer) {
|
||||||
|
clearTimeout(AI_TEXT_STATE.queue_inference_timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function guess_has_webgpu() {
|
||||||
|
console.debug("Best effort to check whether WebGPU is available");
|
||||||
|
if ("gpu" in navigator && navigator.gpu) {
|
||||||
|
console.debug("guess_webgpu: navigator has GPU");
|
||||||
|
try {
|
||||||
|
if (await (navigator.gpu as any).requestAdapter()) {
|
||||||
|
console.debug("guess_webgpu: adapter seems fine");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("guess_webgpu: adapter fail", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
114
extension/src/entrypoints/content.ts
Normal file
114
extension/src/entrypoints/content.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { EXTENSION_CONFIG, ExtensionConfigAiImage, ExtensionConfigAiText } from "@/utils/config";
|
||||||
|
import { traverse_text } from "@/utils/domtraverse";
|
||||||
|
|
||||||
|
export default defineContentScript({
|
||||||
|
matches: ["http://*/*", "https://*/*"],
|
||||||
|
main() {
|
||||||
|
const trigger = setTimeout(() => {
|
||||||
|
text_ai_traverse();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
document.addEventListener("load", () => {
|
||||||
|
clearTimeout(trigger);
|
||||||
|
|
||||||
|
text_ai_traverse();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const TRAVERSE_VIEWPORT_OBSERVER = new IntersectionObserver(
|
||||||
|
(entries, observer) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const stack = TRAVERSE_PARENT_TO_STACK.get(entry.target);
|
||||||
|
|
||||||
|
if (stack) {
|
||||||
|
sendMessage("inference_aiText_classify", { text: stack.text })
|
||||||
|
.then((result) => {
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
|
||||||
|
if (
|
||||||
|
EXTENSION_CONFIG.ai_text.show_ai &&
|
||||||
|
result.prob_ai >= EXTENSION_CONFIG.ai_text.threshold &&
|
||||||
|
result.prob_ai >= result.prob_human
|
||||||
|
) {
|
||||||
|
ai_set_badge(entry.target, result.prob_ai, undefined);
|
||||||
|
} else if (
|
||||||
|
EXTENSION_CONFIG.ai_text.show_human &&
|
||||||
|
result.prob_human >= EXTENSION_CONFIG.ai_text.threshold &&
|
||||||
|
result.prob_human >= result.prob_ai
|
||||||
|
) {
|
||||||
|
ai_set_badge(entry.target, undefined, result.prob_human);
|
||||||
|
} else {
|
||||||
|
ai_clear_badge(entry.target);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rootMargin: "500px",
|
||||||
|
threshold: 0.1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
interface WeakStack {
|
||||||
|
text: string;
|
||||||
|
words: number;
|
||||||
|
first_node?: Node;
|
||||||
|
last_node?: Node;
|
||||||
|
root?: Node;
|
||||||
|
}
|
||||||
|
const TRAVERSE_PARENT_TO_STACK: WeakMap<Element, WeakStack> = new WeakMap();
|
||||||
|
function text_ai_traverse() {
|
||||||
|
for (const stack of traverse_text(600, 20)) {
|
||||||
|
let target = stack.first_node.parentElement || stack.root.parentElement;
|
||||||
|
if (target) {
|
||||||
|
ai_set_badge(target);
|
||||||
|
TRAVERSE_PARENT_TO_STACK.set(target, stack);
|
||||||
|
TRAVERSE_VIEWPORT_OBSERVER.observe(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SET_BADGES: WeakMap<Node, Element> = new WeakMap();
|
||||||
|
function ai_set_badge(node: Node, ai?: number, human?: number) {
|
||||||
|
const badge = document.createElement("span");
|
||||||
|
|
||||||
|
badge.style.display = "inline";
|
||||||
|
badge.style.borderRadius = "4px";
|
||||||
|
badge.style.textAlign = "center";
|
||||||
|
badge.style.fontFamily = "sans";
|
||||||
|
badge.style.fontSize = "12px";
|
||||||
|
badge.style.padding = "1px";
|
||||||
|
badge.style.marginRight = "4px";
|
||||||
|
if (ai || human) {
|
||||||
|
if (ai) {
|
||||||
|
badge.style.backgroundColor = "#ffc36d";
|
||||||
|
badge.style.color = "#fff";
|
||||||
|
badge.textContent = `🤖: ${Math.trunc(ai * 100)}%`;
|
||||||
|
} else if (human) {
|
||||||
|
badge.style.backgroundColor = "#8fe1a1";
|
||||||
|
badge.style.color = "#fff";
|
||||||
|
badge.textContent = `👨💼: ${Math.trunc(human * 100)}%`;
|
||||||
|
} else {
|
||||||
|
badge.style.backgroundColor = "#ddd";
|
||||||
|
badge.style.color = "#fff";
|
||||||
|
badge.textContent = "🦊: confused";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
badge.style.backgroundColor = "#ddd";
|
||||||
|
badge.style.color = "#fff";
|
||||||
|
badge.textContent = "• • • • • •";
|
||||||
|
}
|
||||||
|
|
||||||
|
ai_clear_badge(node);
|
||||||
|
const ins_badge = node.insertBefore(badge, node.firstChild);
|
||||||
|
SET_BADGES.set(node, ins_badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ai_clear_badge(node: Node) {
|
||||||
|
SET_BADGES.get(node)?.remove();
|
||||||
|
}
|
||||||
223
extension/src/entrypoints/popup/App.svelte
Normal file
223
extension/src/entrypoints/popup/App.svelte
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import "./app.css";
|
||||||
|
|
||||||
|
import { extensionConfig } from "@/utils/config";
|
||||||
|
|
||||||
|
class Config<Key extends keyof typeof extensionConfig> {
|
||||||
|
key: Key;
|
||||||
|
value: (typeof extensionConfig)[Key]["fallback"];
|
||||||
|
|
||||||
|
constructor(key: Key) {
|
||||||
|
this.key = key;
|
||||||
|
this.value = $state(extensionConfig[key].fallback);
|
||||||
|
this.load();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
console.log("RESYNC");
|
||||||
|
extensionConfig[key].setValue(this.value as any);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
const config = await extensionConfig[this.key].getValue();
|
||||||
|
|
||||||
|
this.value = config;
|
||||||
|
|
||||||
|
extensionConfig[this.key].watch((value) => {
|
||||||
|
if (value != this.value) this.value = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const config_ai_text = new Config("ai_text");
|
||||||
|
$inspect(config_ai_text.value);
|
||||||
|
|
||||||
|
const config_ai_image = new Config("ai_image");
|
||||||
|
$inspect(config_ai_image.value);
|
||||||
|
|
||||||
|
const config_ai = new Config("ai");
|
||||||
|
$inspect(config_ai.value);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="min-w-115">
|
||||||
|
<div class="tabs tabs-box min-h-50 max-h-max">
|
||||||
|
<div class="tab pr-4">🦊</div>
|
||||||
|
<input type="radio" name="cfgtabs0" class="tab" aria-label="General" checked={true} />
|
||||||
|
<div class="tab-content bg-base-100 border-base-300 p-2"></div>
|
||||||
|
<input type="radio" name="cfgtabs0" class="tab" aria-label="AI detector" />
|
||||||
|
<div class="tab-content bg-base-100 border-base-300 p-2">
|
||||||
|
<fieldset class="fieldset border-base-300 border rounded-box p-2">
|
||||||
|
<legend class="fieldset-legend">AI Text detection</legend>
|
||||||
|
<label class="label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
bind:checked={config_ai_text.value.enabled}
|
||||||
|
/>
|
||||||
|
Enabled
|
||||||
|
</label>
|
||||||
|
<label class="label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
bind:checked={config_ai_text.value.lite}
|
||||||
|
/>
|
||||||
|
Lite version (faster, less accurate)
|
||||||
|
</label>
|
||||||
|
<label class="label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
bind:checked={config_ai_text.value.show_ai}
|
||||||
|
/>
|
||||||
|
Show "AI generated" warnings
|
||||||
|
<span
|
||||||
|
style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#ffc36d;color:#fff"
|
||||||
|
>
|
||||||
|
🤖: 99%
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
bind:checked={config_ai_text.value.show_human}
|
||||||
|
/>
|
||||||
|
Show "Human produced" labels
|
||||||
|
<span
|
||||||
|
style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#8fe1a1;color:#fff"
|
||||||
|
>
|
||||||
|
👨💼: 99%
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="label font-bold" for="textaimodelconf"> Report: </label>
|
||||||
|
<label class="label">
|
||||||
|
Any suspicion
|
||||||
|
<input
|
||||||
|
id="textaimodelconf01"
|
||||||
|
type="range"
|
||||||
|
class="range range-xs"
|
||||||
|
min="0.1"
|
||||||
|
max="0.8"
|
||||||
|
step="0.1"
|
||||||
|
bind:value={config_ai_text.value.threshold}
|
||||||
|
/>
|
||||||
|
When confident
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="fieldset border-base-300 border rounded-box p-2">
|
||||||
|
<legend class="fieldset-legend">AI Image detection</legend>
|
||||||
|
<label class="label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
bind:checked={config_ai_image.value.enabled}
|
||||||
|
/>
|
||||||
|
Enabled
|
||||||
|
</label>
|
||||||
|
<label class="label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
bind:checked={config_ai_image.value.lite}
|
||||||
|
/>
|
||||||
|
Lite version (faster, less accurate)
|
||||||
|
</label>
|
||||||
|
<label class="label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
bind:checked={config_ai_image.value.show_ai}
|
||||||
|
/>
|
||||||
|
Show "AI generated" warnings
|
||||||
|
<span
|
||||||
|
style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#ffc36d;color:#fff"
|
||||||
|
>
|
||||||
|
🤖: 99%
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
bind:checked={config_ai_image.value.show_human}
|
||||||
|
/>
|
||||||
|
Show "Human produced" labels
|
||||||
|
<span
|
||||||
|
style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#8fe1a1;color:#fff"
|
||||||
|
>
|
||||||
|
👨💼: 99%
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="label font-bold" for="textaimodelconf"> Report: </label>
|
||||||
|
<label class="label">
|
||||||
|
Any suspicion
|
||||||
|
<input
|
||||||
|
id="textaimodelconf01"
|
||||||
|
type="range"
|
||||||
|
class="range range-xs"
|
||||||
|
min="0.1"
|
||||||
|
max="0.8"
|
||||||
|
step="0.1"
|
||||||
|
bind:value={config_ai_image.value.threshold}
|
||||||
|
/>
|
||||||
|
When confident
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="fieldset border-base-300 border rounded-box p-2">
|
||||||
|
<legend class="fieldset-legend"> Do not check on these sites: </legend>
|
||||||
|
<div class="overflow-y-auto max-h-30 w-full">
|
||||||
|
{#each Object.entries(config_ai.value.domain_blacklist) as domain (domain[0])}
|
||||||
|
<div class="badge">
|
||||||
|
{domain[0]}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<input type="radio" name="cfgtabs0" class="tab" aria-label="License, citations" />
|
||||||
|
<div class="tab-content bg-base-100 border-base-300 p-2">
|
||||||
|
<p>
|
||||||
|
The source code of this extension is available <a
|
||||||
|
class="link"
|
||||||
|
href={browser.runtime.getManifest().homepage_url}>here</a
|
||||||
|
> under terms of Apache-2.0 license
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
The following people/organizations made this fun extention possible to create by training and
|
||||||
|
providing ML models on <a class="link" href="https://huggingface.co">huggingface</a>:
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<div class="csl-bib-body">
|
||||||
|
<div data-csl-entry-id="dugan-etal-2024-raid" class="csl-entry">
|
||||||
|
Dugan, L., Hwang, A., Trhlı́k, F., Zhu, A., Ludan, J. M., Xu, H., Ippolito, D., &
|
||||||
|
Callison-Burch, C. (2024). RAID: A Shared Benchmark for Robust Evaluation of
|
||||||
|
Machine-Generated Text Detectors. <i
|
||||||
|
>Proceedings of the 62nd Annual Meeting of the Association for Computational
|
||||||
|
Linguistics (Volume 1: Long Papers)</i
|
||||||
|
>, 12463–12492.
|
||||||
|
<a class="link" href="https://aclanthology.org/2024.acl-long.674"
|
||||||
|
>https://aclanthology.org/2024.acl-long.674</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div class="csl-bib-body">
|
||||||
|
<div data-csl-entry-id="drayson2025machine" class="csl-entry">
|
||||||
|
Drayson, G., Yilmaz, E., & Lampos, V. (2025). Machine-generated text detection
|
||||||
|
prevents language model collapse. <i>ArXiv Preprint ArXiv:2502.15654</i>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
... and many many of awesome people who develop and maintain more then 1700 cool things this
|
||||||
|
simple extension requires
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
Let's not forget <b>how many shoulders</b> we're standing on 😉
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
2
extension/src/entrypoints/popup/app.css
Normal file
2
extension/src/entrypoints/popup/app.css
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "daisyui";
|
||||||
7
extension/src/entrypoints/popup/app.ts
Normal file
7
extension/src/entrypoints/popup/app.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { mount } from "svelte";
|
||||||
|
import App from "./App.svelte";
|
||||||
|
import "./app.css";
|
||||||
|
|
||||||
|
export default mount(App, {
|
||||||
|
target: document.getElementById("app")!,
|
||||||
|
});
|
||||||
12
extension/src/entrypoints/popup/index.html
Normal file
12
extension/src/entrypoints/popup/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="manifest.type" content="action" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="./app.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
158
extension/src/utils/config.ts
Normal file
158
extension/src/utils/config.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
import { ExtensionConfigSchema } from "#imports";
|
||||||
|
import { defineExtensionStorage, ExtensionStorage } from "@webext-core/storage";
|
||||||
|
|
||||||
|
export interface ExtensionConfigAiText {
|
||||||
|
enabled: boolean;
|
||||||
|
lite: boolean;
|
||||||
|
show_human: boolean;
|
||||||
|
show_ai: boolean;
|
||||||
|
threshold: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtensionConfigAiImage {
|
||||||
|
enabled: boolean;
|
||||||
|
lite: boolean;
|
||||||
|
show_human: boolean;
|
||||||
|
show_ai: boolean;
|
||||||
|
threshold: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtensionConfigAi {
|
||||||
|
/// shall contain domain qualified name, separated by dots
|
||||||
|
/// indexed by domain name
|
||||||
|
/// e.g. {
|
||||||
|
/// "github.com": ["github", "com"]
|
||||||
|
/// }
|
||||||
|
domain_blacklist: {
|
||||||
|
[domain: string]: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// export interface ExtensionConfigSchema {
|
||||||
|
// version: string;
|
||||||
|
// ai_text: ExtensionConfigAiText;
|
||||||
|
// ai_image: ExtensionConfigAiImage;
|
||||||
|
// }
|
||||||
|
|
||||||
|
//export const extensionConfig = defineExtensionStorage<ExtensionConfigSchema>(browser.storage.local);
|
||||||
|
|
||||||
|
export const extensionConfig = {
|
||||||
|
ai: storage.defineItem<ExtensionConfigAi>("local:config:ai", {
|
||||||
|
fallback: {
|
||||||
|
domain_blacklist: {},
|
||||||
|
},
|
||||||
|
init: () => ({
|
||||||
|
domain_blacklist: {},
|
||||||
|
}),
|
||||||
|
version: 1,
|
||||||
|
migrations: {},
|
||||||
|
}),
|
||||||
|
ai_text: storage.defineItem<ExtensionConfigAiText>("local:config:ai_text", {
|
||||||
|
fallback: {
|
||||||
|
enabled: true,
|
||||||
|
lite: true,
|
||||||
|
show_ai: true,
|
||||||
|
show_human: false,
|
||||||
|
threshold: 0.6,
|
||||||
|
},
|
||||||
|
init: () => ({
|
||||||
|
enabled: true,
|
||||||
|
lite: true,
|
||||||
|
show_ai: true,
|
||||||
|
show_human: false,
|
||||||
|
threshold: 0.6,
|
||||||
|
}),
|
||||||
|
version: 1,
|
||||||
|
migrations: {},
|
||||||
|
}),
|
||||||
|
ai_image: storage.defineItem<ExtensionConfigAiImage>("local:config:ai_image", {
|
||||||
|
fallback: {
|
||||||
|
enabled: true,
|
||||||
|
lite: true,
|
||||||
|
show_ai: true,
|
||||||
|
show_human: false,
|
||||||
|
threshold: 0.6,
|
||||||
|
},
|
||||||
|
init: () => ({
|
||||||
|
enabled: true,
|
||||||
|
lite: true,
|
||||||
|
show_ai: true,
|
||||||
|
show_human: true,
|
||||||
|
threshold: 0.6,
|
||||||
|
}),
|
||||||
|
version: 1,
|
||||||
|
migrations: {},
|
||||||
|
}),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const EXTENSION_CONFIG: {
|
||||||
|
ai: ExtensionConfigAi;
|
||||||
|
ai_text: ExtensionConfigAiText;
|
||||||
|
ai_image: ExtensionConfigAiImage;
|
||||||
|
} = {
|
||||||
|
ai: extensionConfig.ai.fallback,
|
||||||
|
ai_text: extensionConfig.ai_text.fallback,
|
||||||
|
ai_image: extensionConfig.ai_image.fallback,
|
||||||
|
};
|
||||||
|
|
||||||
|
extensionConfig.ai.getValue().then((config) => {
|
||||||
|
EXTENSION_CONFIG.ai = config;
|
||||||
|
|
||||||
|
extensionConfig.ai.watch((config) => {
|
||||||
|
EXTENSION_CONFIG.ai = config;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
extensionConfig.ai_text.getValue().then((config) => {
|
||||||
|
EXTENSION_CONFIG.ai_text = config;
|
||||||
|
|
||||||
|
extensionConfig.ai_text.watch((config) => {
|
||||||
|
EXTENSION_CONFIG.ai_text = config;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
extensionConfig.ai_image.getValue().then((config) => {
|
||||||
|
EXTENSION_CONFIG.ai_image = config;
|
||||||
|
|
||||||
|
extensionConfig.ai_image.watch((config) => {
|
||||||
|
EXTENSION_CONFIG.ai_image = config;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// extensionConfig.getItem("version").then(async (v) => {
|
||||||
|
// const current_version = browser.runtime.getManifest().version;
|
||||||
|
// if (v != current_version) {
|
||||||
|
// switch (v) {
|
||||||
|
// default:
|
||||||
|
// await config_migrate_fresh();
|
||||||
|
// }
|
||||||
|
// await extensionConfig.setItem("version", current_version);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// async function config_migrate_fresh() {
|
||||||
|
// await extensionConfig.clear();
|
||||||
|
// await extensionConfig.setItem("ai_text", {
|
||||||
|
// enabled: true,
|
||||||
|
// lite: true,
|
||||||
|
// show_ai: true,
|
||||||
|
// show_human: false,
|
||||||
|
// threshold: 0.6,
|
||||||
|
// });
|
||||||
|
// await extensionConfig.setItem("ai_image", {
|
||||||
|
// enabled: true,
|
||||||
|
// lite: true,
|
||||||
|
// show_ai: true,
|
||||||
|
// show_human: false,
|
||||||
|
// threshold: 0.6,
|
||||||
|
// });
|
||||||
|
// await extensionConfig.setItem("fun_githief", {
|
||||||
|
// enabled: false,
|
||||||
|
// });
|
||||||
|
// await extensionConfig.setItem("fun_microslop", {
|
||||||
|
// enabled: false,
|
||||||
|
// });
|
||||||
|
// await extensionConfig.setItem("fun_huyandex", {
|
||||||
|
// enabled: false,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
211
extension/src/utils/domtraverse.ts
Normal file
211
extension/src/utils/domtraverse.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
/// Extracts meaningful portions of text from DOM, yielding the top matched nodes
|
||||||
|
///
|
||||||
|
/// e.g.
|
||||||
|
///
|
||||||
|
/// ```html
|
||||||
|
/// <div>
|
||||||
|
/// my hair are NOT
|
||||||
|
/// <span>
|
||||||
|
/// blue!
|
||||||
|
/// </span>
|
||||||
|
/// </div>
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Shall yield a reference to the `div` with text "my hair are NOT blue!"
|
||||||
|
|
||||||
|
const IS_DEV = import.meta.env.MODE == "development";
|
||||||
|
|
||||||
|
const TRAVERSE_STACK_BANS: Set<string | null | undefined> = new Set([
|
||||||
|
"IMG",
|
||||||
|
"FIGURE",
|
||||||
|
"SVG",
|
||||||
|
"VIDEO",
|
||||||
|
"META",
|
||||||
|
]);
|
||||||
|
const TRAVERSE_STACK_ROOTS: Set<string | null | undefined> = new Set([
|
||||||
|
"DIV",
|
||||||
|
"ARTICLE",
|
||||||
|
"SECTION",
|
||||||
|
"MAIN",
|
||||||
|
"HEADER",
|
||||||
|
"BLOCKQUOTE",
|
||||||
|
]);
|
||||||
|
const TRAVERSE_ALLOWED_ELEMENT_TAGS: Set<string | null | undefined> = new Set([
|
||||||
|
...TRAVERSE_STACK_ROOTS,
|
||||||
|
"SPAN",
|
||||||
|
"P",
|
||||||
|
"H1",
|
||||||
|
"H2",
|
||||||
|
"H3",
|
||||||
|
"H4",
|
||||||
|
"H5",
|
||||||
|
"H6",
|
||||||
|
"H7",
|
||||||
|
"H8",
|
||||||
|
"UL",
|
||||||
|
"OL",
|
||||||
|
"LI",
|
||||||
|
"DEL",
|
||||||
|
"B",
|
||||||
|
"I",
|
||||||
|
"STRONG",
|
||||||
|
"EM",
|
||||||
|
]);
|
||||||
|
export function* traverse_text(soft_max_words: number, hard_min_words: number) {
|
||||||
|
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
|
||||||
|
acceptNode: (node) => {
|
||||||
|
// sanity check
|
||||||
|
if (!node.isConnected) {
|
||||||
|
if (IS_DEV) {
|
||||||
|
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-!IS_CONNECTED");
|
||||||
|
}
|
||||||
|
return NodeFilter.FILTER_REJECT;
|
||||||
|
}
|
||||||
|
if (node.parentNode?.nodeType != Node.ELEMENT_NODE) {
|
||||||
|
if (IS_DEV) {
|
||||||
|
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-!ELEMENT_NODE");
|
||||||
|
}
|
||||||
|
return NodeFilter.FILTER_REJECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter elements
|
||||||
|
if (!TRAVERSE_ALLOWED_ELEMENT_TAGS.has(node.parentElement?.tagName)) {
|
||||||
|
if (IS_DEV) {
|
||||||
|
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-!ALLOWED_TAG");
|
||||||
|
}
|
||||||
|
return NodeFilter.FILTER_REJECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: figure out more performant way to check this.
|
||||||
|
// Ignore anything inside of banned elements.
|
||||||
|
//
|
||||||
|
// For now I'll just limit the depth to 16
|
||||||
|
let checking = node.parentElement;
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
if (!checking || checking.tagName == "BODY") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (TRAVERSE_STACK_BANS.has(checking.tagName)) {
|
||||||
|
if (IS_DEV) {
|
||||||
|
node.parentElement?.setAttribute(
|
||||||
|
"data-firaid-moz-traverse",
|
||||||
|
"REJECT-BANNED_ANCESTOR",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NodeFilter.FILTER_REJECT;
|
||||||
|
}
|
||||||
|
checking = checking.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
// drop with nonsense content
|
||||||
|
if ((node.textContent?.trim()?.length || 0) < 8) {
|
||||||
|
if (IS_DEV) {
|
||||||
|
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-SHORT_TEXT");
|
||||||
|
}
|
||||||
|
return NodeFilter.FILTER_REJECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally good ones
|
||||||
|
if (IS_DEV) {
|
||||||
|
node.parentElement?.setAttribute("data-firaid-moz-traverse", "ACCEPT");
|
||||||
|
}
|
||||||
|
return NodeFilter.FILTER_ACCEPT;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let stack: { root: Node; first_node: Node; last_node: Node; text: string; words: number } | null = null;
|
||||||
|
|
||||||
|
while (walker.nextNode()) {
|
||||||
|
const node = walker.currentNode;
|
||||||
|
|
||||||
|
let rootable_parent = node.parentElement;
|
||||||
|
while (
|
||||||
|
rootable_parent &&
|
||||||
|
rootable_parent.tagName != "HTML" &&
|
||||||
|
!TRAVERSE_STACK_ROOTS.has(rootable_parent.tagName)
|
||||||
|
) {
|
||||||
|
rootable_parent = rootable_parent.parentElement;
|
||||||
|
}
|
||||||
|
if (!rootable_parent) {
|
||||||
|
if (IS_DEV) {
|
||||||
|
node.parentElement?.setAttribute("data-firaid-moz-stack", "REJECTED-!ROOTABLE_PARENT");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodeText = node.textContent?.trim();
|
||||||
|
if (!nodeText) {
|
||||||
|
if (IS_DEV) {
|
||||||
|
node.parentElement?.setAttribute("data-firaid-moz-stack", "REJECTED-!TEXT");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let nodeTextWords = count_words_naive(nodeText);
|
||||||
|
|
||||||
|
if (IS_DEV) {
|
||||||
|
node.parentElement?.setAttribute("data-firaid-moz-stack", "VISITED");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stack) {
|
||||||
|
stack = {
|
||||||
|
root: rootable_parent,
|
||||||
|
first_node: node,
|
||||||
|
last_node: node,
|
||||||
|
text: nodeText,
|
||||||
|
words: nodeTextWords,
|
||||||
|
};
|
||||||
|
} else if (stack.root.contains(rootable_parent)) {
|
||||||
|
if (IS_DEV) {
|
||||||
|
node.parentElement?.setAttribute("data-firaid-moz-stack", "MEMBER");
|
||||||
|
}
|
||||||
|
stack.text += " " + nodeText;
|
||||||
|
stack.words += nodeTextWords;
|
||||||
|
stack.last_node = node;
|
||||||
|
} else {
|
||||||
|
if (stack.words >= hard_min_words) {
|
||||||
|
yield stack;
|
||||||
|
if (IS_DEV) {
|
||||||
|
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "ROOT");
|
||||||
|
}
|
||||||
|
} else if (IS_DEV) {
|
||||||
|
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "SKIP-SHORT_TEXT");
|
||||||
|
}
|
||||||
|
stack = {
|
||||||
|
root: rootable_parent,
|
||||||
|
first_node: node,
|
||||||
|
last_node: node,
|
||||||
|
text: nodeText,
|
||||||
|
words: nodeTextWords,
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stack.words > soft_max_words) {
|
||||||
|
if (stack.words >= hard_min_words) {
|
||||||
|
if (IS_DEV) {
|
||||||
|
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "ROOT");
|
||||||
|
}
|
||||||
|
} else if (IS_DEV) {
|
||||||
|
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "SKIP-SHORT_TEXT");
|
||||||
|
}
|
||||||
|
stack = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function count_words_naive(text: string): number {
|
||||||
|
const is_space = /\s/;
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
let word_marker = false;
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
if (is_space.test(text[i])) {
|
||||||
|
word_marker = false;
|
||||||
|
} else if (!word_marker) {
|
||||||
|
count += 1;
|
||||||
|
word_marker = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
25
extension/src/utils/logging.ts
Normal file
25
extension/src/utils/logging.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { sendMessage } from "@/utils/messaging";
|
||||||
|
|
||||||
|
export enum ExtLogLevel {
|
||||||
|
Debug,
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExtConsole = (sender: string) => {
|
||||||
|
return {
|
||||||
|
debug(...args: any[]) {
|
||||||
|
sendMessage("log", { level: ExtLogLevel.Debug, sender, args });
|
||||||
|
},
|
||||||
|
log(...args: any[]) {
|
||||||
|
sendMessage("log", { level: ExtLogLevel.Info, sender, args });
|
||||||
|
},
|
||||||
|
warn(...args: any[]) {
|
||||||
|
sendMessage("log", { level: ExtLogLevel.Warning, sender, args });
|
||||||
|
},
|
||||||
|
error(...args: any[]) {
|
||||||
|
sendMessage("log", { level: ExtLogLevel.Error, sender, args });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
9
extension/src/utils/messaging.ts
Normal file
9
extension/src/utils/messaging.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { defineExtensionMessaging } from "@webext-core/messaging";
|
||||||
|
import { ExtLogLevel } from "@/utils/logging";
|
||||||
|
|
||||||
|
interface ProtocolMap {
|
||||||
|
inference_aiText_classify(data: { text: string }): { prob_ai: number; prob_human: number };
|
||||||
|
log(data: { level: ExtLogLevel; sender: string; args: any[] }): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { sendMessage, onMessage } = defineExtensionMessaging<ProtocolMap>();
|
||||||
7
extension/tsconfig.json
Normal file
7
extension/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "./.wxt/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
}
|
||||||
5
extension/web-ext.config.ts
Normal file
5
extension/web-ext.config.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { defineWebExtConfig } from "wxt";
|
||||||
|
|
||||||
|
export default defineWebExtConfig({
|
||||||
|
chromiumArgs: ["--enable-features=Vulkan"],
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue