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
18 changed files with 2068 additions and 0 deletions

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

21
extension/wxt.config.ts Normal file
View file

@ -0,0 +1,21 @@
import { defineConfig } from "wxt";
import tailwindcss from "@tailwindcss/vite";
// See https://wxt.dev/api/config.html
export default defineConfig({
srcDir: "src",
modules: ["@wxt-dev/module-svelte"],
manifest: {
permissions: ["storage", "activeTab", "contextMenus", "pageCapture", "offscreen"],
web_accessible_resources: [
{
matches: ["<all_urls>"],
resources: ["/inference-ai-text_bg.wasm", "/inference-ai-text-lite_bg.wasm"],
},
],
homepage_url: "https://source.spushk.ru/pub/firaid-moz",
},
vite: () => ({
plugins: [tailwindcss()],
}),
});

1
extension/wxt.env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="svelte" />