This commit is contained in:
masters 2026-03-11 02:36:36 +05:00 • committed by spushk
commit f2b8bdca98
Signed by: spushk
GPG key ID: 7797B761E105E8AA
33 changed files with 2068 additions and 0 deletions

9
.gitignore vendored Normal file
View 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
View 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
View 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

View file

@ -0,0 +1,2 @@
optimum-onnx[onnxruntime]
accelerate

View 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
View 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"
}
}

View file

@ -0,0 +1,7 @@
import { defineAppConfig } from "#imports";
declare module "wxt/utils/define-app-config" {
export interface WxtAppConfig {}
}
export default defineAppConfig({});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View 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;
}

View 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();
}

View 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., &#38;
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., &#38; 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>

View file

@ -0,0 +1,2 @@
@import "tailwindcss";
@plugin "daisyui";

View 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")!,
});

View 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>

View 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,
// });
// }

View 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;
}

View 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 });
},
};
};

View 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
View file

@ -0,0 +1,7 @@
{
"extends": "./.wxt/tsconfig.json",
"compilerOptions": {
"useDefineForClassFields": true,
},
"files": [],
}

View 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