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