Still WIP
This commit is contained in:
parent
2109aaa16d
commit
6d28a2f53a
25 changed files with 690 additions and 383 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -6,4 +6,4 @@ extension/.output
|
||||||
extension/.wxt
|
extension/.wxt
|
||||||
extension/node_modules
|
extension/node_modules
|
||||||
extension/pnpm-lock.yaml
|
extension/pnpm-lock.yaml
|
||||||
.export_venv
|
.envrc
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[tasks.inference_model_ai_text]
|
[tasks.inference_model_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"}
|
env = {_MK_MODEL_AI_TEXT = "onnx-community/modernbert-ai-detection-raid-mage-ONNX", _MK_MODEL_AI_TEXT_ONNX = "onnx/model_fp16.onnx", _MK_MODEL_AI_TEXT_ATTENTION = "yes", _MK_MODEL_AI_TEXT_TYPES = "no"}
|
||||||
cwd = "wasm/inference-models"
|
cwd = "wasm/inference-models"
|
||||||
install_crate = "wasm-pack"
|
install_crate = "wasm-pack"
|
||||||
command = "wasm-pack"
|
command = "wasm-pack"
|
||||||
|
|
@ -75,12 +75,11 @@ args = [
|
||||||
"--target", "web",
|
"--target", "web",
|
||||||
"--out-dir", "pkg/inference-ai-text",
|
"--out-dir", "pkg/inference-ai-text",
|
||||||
"--out-name", "inference-ai-text",
|
"--out-name", "inference-ai-text",
|
||||||
"--features", "ai_text",
|
"--features", "ai_text"
|
||||||
"-vvv"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tasks.inference_model_ai_text_lite]
|
[tasks.inference_model_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"}
|
env = {_MK_MODEL_AI_TEXT = "onnx-community/e5-small-lora-ai-generated-detector-ONNX", _MK_MODEL_AI_TEXT_ONNX = "onnx/model_fp16.onnx", _MK_MODEL_AI_TEXT_ATTENTION = "yes", _MK_MODEL_AI_TEXT_TYPES = "yes"}
|
||||||
cwd = "wasm/inference-models"
|
cwd = "wasm/inference-models"
|
||||||
install_crate = "wasm-pack"
|
install_crate = "wasm-pack"
|
||||||
command = "wasm-pack"
|
command = "wasm-pack"
|
||||||
|
|
|
||||||
117
README.md
117
README.md
|
|
@ -1,5 +1,28 @@
|
||||||
# firaid-moz
|
# firaid-moz
|
||||||
> I don't know why I named it this way
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This software is provided under terms of BUSL 1.1 license in most permissive mode,
|
||||||
|
> with exceptions for some automation.
|
||||||
|
> Please, consult the [LICENSE](LICENSE.md) if you're about to perform AI-assisted analysis
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Disclaimer:**
|
||||||
|
|
||||||
|
The software provided as-is, without warranty of any kind, express or implied.
|
||||||
|
This project is an educational proof-of-concept experiment.
|
||||||
|
|
||||||
|
To clarify:
|
||||||
|
|
||||||
|
- I am not responsible for your judgement on content origin (AI/human/assisted)
|
||||||
|
- I am not responsible for your attitude towards AI/human/assisted content
|
||||||
|
- I am not responsible for any consequences of such a judgement
|
||||||
|
|
||||||
|
Classifications performed by this extension are not based on private or social opinions. This extension does not rely on reputations, lists, filters, etc.
|
||||||
|
|
||||||
|
The only method used by this extension to determine origin of content is a bunch of linear algebra in a black-box, aka "ML model", which decisions may or may not be biased - depending on training dataset, which is out of scope of this project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
A simple local webextension, which can run a bunch of small ML models to infer some properties of DOM content, such as:
|
A simple local webextension, which can run a bunch of small ML models to infer some properties of DOM content, such as:
|
||||||
|
|
||||||
|
|
@ -8,61 +31,93 @@ A simple local webextension, which can run a bunch of small ML models to infer s
|
||||||
|
|
||||||
## AI/Human classification
|
## 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.
|
> **NONE** of ML/AI/whatever models is 100% accurate by the very fundamental design. Every "AI" simply *pretends* to posess some information. 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!
|
> If you don't trust the "AI slop" - you should not completely trust this extension also!
|
||||||
>
|
>
|
||||||
> False-positives? - Possible
|
> False-positives - Possible
|
||||||
>
|
>
|
||||||
> False-negatives? - Probably
|
> False-negatives - Probably
|
||||||
>
|
>
|
||||||
> This extension - like **every other AI** - is a **tool** which aims to help you and **not** replace your own discretion.
|
> User discretion - Highly advised
|
||||||
|
|
||||||
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?
|
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.
|
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):
|
AI/Human classification can be done in three modes:
|
||||||
|
|
||||||
0. _(potato)_: No classification
|
1. _(lite)_: Lightweight model
|
||||||
1. _(normal)_: Lightweight model, which can have "no decision at all" in some cases
|
2. _(mixed)_: Lightweight model runs first, but when its decision is weak - heavier model takes the task. This is usually faster, but requires both lite and heavy models to be loaded thus eating lots of RAM
|
||||||
2. _(high)_: Lightweight model still does its job, but when its decision is weak - heavy model takes the task
|
3. _(hard)_: Heavier and more accurate model takes all tasks, lightweight model is disabled
|
||||||
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 _lite_ mode requires ~80Mb of RAM for text classification
|
||||||
|
|
||||||
The _ultra_ mode requires ~700Mb of RAM for text classification and ~500Mb of RAM for image classification.
|
The _mixed_ mode requires ~380Mb of RAM for text classification
|
||||||
|
|
||||||
## `Microslop` mode
|
The _hard_ mode requires ~300Mb of RAM for text classification
|
||||||
|
|
||||||
> Slop-sloppity slop, micro slop slop slop, microslop microslop microslop
|
## Installation
|
||||||
|
|
||||||
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"
|
### Building from source
|
||||||
|
|
||||||
## `Githief` mode
|
This webextension can be built from source. The following instrumentation is required:
|
||||||
|
|
||||||
You know what they do with your "licensed" and "private repository" code.
|
- `cargo` + `cargo-make` + `wasm-pack` (cargo-make will install it automatically)
|
||||||
|
- `pnpm`
|
||||||
|
|
||||||
Turn this on - and all "github" (except for urls) shall turn into "githief"
|
To build both firefox and chrome release versions, run the `release` task:
|
||||||
|
|
||||||
## `Хуяндекс` mode
|
```bash
|
||||||
|
cargo make -p production release
|
||||||
|
```
|
||||||
|
|
||||||
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.
|
If you do not have `wasm-pack` (cargo) installed, cargo-make will install it for you.
|
||||||
|
|
||||||
You can read more about it [here](https://localmess.github.io/)
|
#### Models
|
||||||
|
|
||||||
They stopped that practice after disclosure, yes, but they did it nontheless.
|
To only build models, clone the repo, and run `models` task within `production` environment:
|
||||||
|
|
||||||
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).
|
```bash
|
||||||
|
cargo make -p production models
|
||||||
|
```
|
||||||
|
|
||||||
---
|
This will compile all implemented models into WASM packages, which are later used by webextension.
|
||||||
|
|
||||||
## A bit of technical details
|
NPM-ready packages for both ai-text models will be located under `<repo>/wasm/pkg` directory
|
||||||
|
|
||||||
## Contributing
|
##### Using different models for inference
|
||||||
|
|
||||||
Wooow, I am so very glad thay you're reading this section! Thank you in advance for interest to contributing.
|
You can build the [text classification](./wasm/model-ai-text) with any ONNX model from huggingface.
|
||||||
|
|
||||||
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.
|
Following env variables are required:
|
||||||
|
|
||||||
Also, if you sumbit any code, it is considered Apache-2.0
|
- `_MK_MODEL_AI_TEXT`: name of the huggingface repository (e.g. "onnx-community/text-ai-detection")
|
||||||
|
- `_MK_MODEL_AI_TEXT_ONNX`: name of the onnx model file under the repository (e.g. "onnx/model.onnx")
|
||||||
|
|
||||||
|
Following env variables are optional:
|
||||||
|
|
||||||
|
- `_MK_MODEL_AI_TEXT_ATTENTION`: set to `yes` or `1` if your model uses attention mask
|
||||||
|
- `_MK_MODEL_AI_TEXT_TYPES`: set to `yes` or `1` if your model uses token types tensor
|
||||||
|
|
||||||
|
Not every model will work, and not every model which works will compute valid results.
|
||||||
|
Major constraints are:
|
||||||
|
|
||||||
|
- FP32 or FP16 weights (quantizations, int8, etc will be available when implemented by burn/burn-onnx)
|
||||||
|
- Sane size, 500Mb is your soft ceiling (browsers would likely refuse to load enormous WASM)
|
||||||
|
- Two label classification, where label `[0]` is Human and `[1]` is AI
|
||||||
|
- Output is `[0, 2]` sized tensor of *logits*
|
||||||
|
|
||||||
|
If you encounter compilation error, and compiler complains about invalid `forward` function arguments,
|
||||||
|
most likely you set the wrong `_MK_MODEL_AI_TEXT_ATTENTION` and `_MK_MODEL_AI_TEXT_TYPES` values.
|
||||||
|
|
||||||
|
#### Extension
|
||||||
|
|
||||||
|
> You must build models before running this
|
||||||
|
|
||||||
|
To only build the extension, clone the repo, build models, then you can run following tasks:
|
||||||
|
|
||||||
|
- `extension`: build and zip firefox and chrome extensions
|
||||||
|
- `extension_moz`: only build firefox extension
|
||||||
|
- `extension_chro`: only build chrome extension
|
||||||
|
- `dev`: run wxt dev server (chrome variant)
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
optimum-onnx[onnxruntime]
|
|
||||||
accelerate
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
39
extension/src/entrypoints/background/index.ts
Normal file
39
extension/src/entrypoints/background/index.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import text_inference, { queue as text_queue } from "./text_inference";
|
||||||
|
|
||||||
|
import { onMessage } from "@/utils/messaging";
|
||||||
|
import { ExtLogLevel } from "@/utils/logging";
|
||||||
|
import { ExtensionConfigAiText } from "@/utils/config";
|
||||||
|
|
||||||
|
export default defineBackground({
|
||||||
|
type: "module",
|
||||||
|
main() {
|
||||||
|
setup_ext_console();
|
||||||
|
setup_text_inference();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup_text_inference() {
|
||||||
|
text_inference();
|
||||||
|
onMessage("inference_aiText_classify", ({ data }) => {
|
||||||
|
return text_queue(data.text);
|
||||||
|
});
|
||||||
|
}
|
||||||
184
extension/src/entrypoints/background/text_inference.ts
Normal file
184
extension/src/entrypoints/background/text_inference.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import { ExtensionConfigAiText, SyncingExtensionConfig } from "@/utils/config";
|
||||||
|
import inference_ai_text, { AiText, AiTextInference, Capability } from "inference-ai-text";
|
||||||
|
import inference_ai_text_lite, {
|
||||||
|
AiText as AiTextLite,
|
||||||
|
Capability as CapabilityLite,
|
||||||
|
} from "inference-ai-text-lite";
|
||||||
|
import { has_wgpu } from "@/utils/system";
|
||||||
|
|
||||||
|
const CONFIG = new SyncingExtensionConfig("ai_text");
|
||||||
|
|
||||||
|
const STATE: {
|
||||||
|
handle: Promise<AiText> | null;
|
||||||
|
handle_lite: Promise<AiTextLite> | null;
|
||||||
|
handle_run_timer: ReturnType<typeof setTimeout> | null;
|
||||||
|
handle_lite_run_timer: ReturnType<typeof setTimeout> | null;
|
||||||
|
wasm_load: boolean;
|
||||||
|
wasm_load_lite: boolean;
|
||||||
|
} = {
|
||||||
|
handle: null,
|
||||||
|
handle_lite: null,
|
||||||
|
handle_run_timer: null,
|
||||||
|
handle_lite_run_timer: null,
|
||||||
|
wasm_load: false,
|
||||||
|
wasm_load_lite: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function setup() {
|
||||||
|
CONFIG.sync.then(() => {
|
||||||
|
reload(CONFIG.value);
|
||||||
|
CONFIG.watch(reload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function queue(text: string): Promise<{ prob_ai: number; prob_human: number }> {
|
||||||
|
await CONFIG.sync;
|
||||||
|
|
||||||
|
if (!CONFIG.value.enabled) {
|
||||||
|
console.debug("text inference disabled");
|
||||||
|
return new Promise((_, rej) =>
|
||||||
|
rej({
|
||||||
|
reason: "disabled",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle = await STATE.handle;
|
||||||
|
const handle_lite = await STATE.handle_lite;
|
||||||
|
|
||||||
|
const result_promise: Promise<{ prob_ai: number; prob_human: number }> = new Promise(async (res, rej) => {
|
||||||
|
let result: AiTextInference | null = null;
|
||||||
|
|
||||||
|
switch (CONFIG.value.mode) {
|
||||||
|
case "mixed":
|
||||||
|
result = (await handle_lite?.queue(text)) || null;
|
||||||
|
break;
|
||||||
|
case "lite":
|
||||||
|
result = (await handle_lite?.queue(text)) || null;
|
||||||
|
break;
|
||||||
|
case "hard":
|
||||||
|
result = (await handle?.queue(text)) || null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
console.warn("Configured inference model was not available");
|
||||||
|
rej({
|
||||||
|
reason: "no_model",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mixed case
|
||||||
|
if (CONFIG.value.mode == "mixed" && result.prob_ai + result.prob_human < 0.8) {
|
||||||
|
console.debug("mixed mode: first inference was not confident");
|
||||||
|
const second_result = (await (await STATE.handle)?.queue(text)) || null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
second_result &&
|
||||||
|
second_result.prob_ai + second_result.prob_human >= result.prob_ai + result.prob_human
|
||||||
|
) {
|
||||||
|
result = second_result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
rej({
|
||||||
|
reason: "no_result",
|
||||||
|
});
|
||||||
|
else res(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
const stats = handle?.stats();
|
||||||
|
const stats_lite = handle_lite?.stats();
|
||||||
|
|
||||||
|
// normal model inference queue check / run
|
||||||
|
if (STATE.handle_run_timer) {
|
||||||
|
clearTimeout(STATE.handle_run_timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats && stats.queue_size > stats.queue_limit / 2) {
|
||||||
|
// immediate inference on threshold
|
||||||
|
handle?.run_queued_inference().catch((e) => {
|
||||||
|
console.error("Failed to run inference", e);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
STATE.handle_run_timer = setTimeout(() => {
|
||||||
|
handle?.run_queued_inference().catch((e) => {
|
||||||
|
console.error("Failed to run inference", e);
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lite model inference queue chech / run
|
||||||
|
if (STATE.handle_lite_run_timer) {
|
||||||
|
clearTimeout(STATE.handle_lite_run_timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats_lite && stats_lite.queue_size > stats_lite.queue_limit / 2) {
|
||||||
|
// immediate inference on threshold
|
||||||
|
handle_lite?.run_queued_inference().catch((e) => {
|
||||||
|
console.error("Failed to run inference (lite)", e);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
STATE.handle_lite_run_timer = setTimeout(() => {
|
||||||
|
handle_lite?.run_queued_inference().catch((e) => {
|
||||||
|
console.error("Failed to run inference (lite)", e);
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result_promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload(config: ExtensionConfigAiText) {
|
||||||
|
if (!config.enabled) return;
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
if (!STATE.wasm_load) {
|
||||||
|
await inference_ai_text();
|
||||||
|
if (await has_wgpu()) {
|
||||||
|
console.warn(
|
||||||
|
"WebGPU seems available, but burn currently does not support GPU on WASM. See the () issue. Use NdArray backend anyway",
|
||||||
|
);
|
||||||
|
Capability.assert_ndarray();
|
||||||
|
///Capability.assert_webgpu();
|
||||||
|
} else {
|
||||||
|
Capability.assert_ndarray();
|
||||||
|
}
|
||||||
|
STATE.wasm_load = true;
|
||||||
|
}
|
||||||
|
return new AiText();
|
||||||
|
};
|
||||||
|
const load_lite = async () => {
|
||||||
|
if (!STATE.wasm_load_lite) {
|
||||||
|
await inference_ai_text_lite();
|
||||||
|
if (await has_wgpu()) {
|
||||||
|
console.warn(
|
||||||
|
"WebGPU seems available, but burn currently does not support GPU on WASM. See the () issue. Use NdArray backend anyway",
|
||||||
|
);
|
||||||
|
CapabilityLite.assert_ndarray();
|
||||||
|
///CapabilityLite.assert_webgpu();
|
||||||
|
} else {
|
||||||
|
CapabilityLite.assert_ndarray();
|
||||||
|
}
|
||||||
|
STATE.wasm_load_lite = true;
|
||||||
|
}
|
||||||
|
return new AiTextLite();
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (config.mode) {
|
||||||
|
case "mixed":
|
||||||
|
STATE.handle = load();
|
||||||
|
STATE.handle_lite = load_lite();
|
||||||
|
break;
|
||||||
|
case "lite":
|
||||||
|
STATE.handle = null;
|
||||||
|
STATE.handle_lite = load_lite();
|
||||||
|
break;
|
||||||
|
case "hard":
|
||||||
|
STATE.handle_lite = null;
|
||||||
|
STATE.handle = load();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
import { EXTENSION_CONFIG, ExtensionConfigAiImage, ExtensionConfigAiText } from "@/utils/config";
|
|
||||||
import { traverse_text } from "@/utils/domtraverse";
|
|
||||||
|
|
||||||
const console = ExtConsole("content");
|
|
||||||
|
|
||||||
export default defineContentScript({
|
|
||||||
matches: ["http://*/*", "https://*/*"],
|
|
||||||
main() {
|
|
||||||
DOM_OBSERVATORY.observer = new MutationObserver((_) => {
|
|
||||||
if (DOM_OBSERVATORY.timer) {
|
|
||||||
clearTimeout(DOM_OBSERVATORY.timer);
|
|
||||||
}
|
|
||||||
DOM_OBSERVATORY.timer = setTimeout(() => {
|
|
||||||
console.debug("retraverse, cause: mutation");
|
|
||||||
text_ai_traverse();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
text_ai_traverse();
|
|
||||||
|
|
||||||
DOM_OBSERVATORY.observer?.observe(document.body, {
|
|
||||||
subtree: true,
|
|
||||||
characterData: true,
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const DOM_OBSERVATORY = {
|
|
||||||
observer: null as MutationObserver | null,
|
|
||||||
timer: null as number | null,
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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() {
|
|
||||||
console.debug("text_ai_traverse: traverse DOM");
|
|
||||||
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.setAttribute(TRAVERSE_BEHAVOUR_ATTR, TRAVERSE_BEHAVOUR_IGNORE_MAGIC);
|
|
||||||
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) {
|
|
||||||
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 = "• • • • • •";
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
23
extension/src/entrypoints/content/index.ts
Normal file
23
extension/src/entrypoints/content/index.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { SyncingExtensionConfig } from "@/utils/config";
|
||||||
|
import text_class, { traverse as text_traverse } from "./text_class";
|
||||||
|
|
||||||
|
const console = ExtConsole("content");
|
||||||
|
|
||||||
|
const CONFIG_DOMAIN_WHITELIST = new SyncingExtensionConfig("domain_whitelist");
|
||||||
|
|
||||||
|
export default defineContentScript({
|
||||||
|
matches: ["http://*/*", "https://*/*"],
|
||||||
|
main() {
|
||||||
|
const text_set = () => {
|
||||||
|
text_class();
|
||||||
|
text_traverse();
|
||||||
|
};
|
||||||
|
let text_timer = setTimeout(text_set, 1000);
|
||||||
|
window.onload = () => {
|
||||||
|
clearTimeout(text_timer);
|
||||||
|
text_set();
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("ready");
|
||||||
|
},
|
||||||
|
});
|
||||||
125
extension/src/entrypoints/content/text_class.ts
Normal file
125
extension/src/entrypoints/content/text_class.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { TRAVERSE_BEHAVOUR_ATTR, TRAVERSE_BEHAVOUR_IGNORE_MAGIC, traverse_text } from "@/utils/domtraverse";
|
||||||
|
import { SyncingExtensionConfig } from "@/utils/config";
|
||||||
|
import { ExtConsole } from "@/utils/logging";
|
||||||
|
import { sendMessage } from "@/utils/messaging";
|
||||||
|
|
||||||
|
const console = ExtConsole("contentscript::text_class");
|
||||||
|
|
||||||
|
const CONFIG = new SyncingExtensionConfig("ai_text");
|
||||||
|
|
||||||
|
const DOM_OBSERVATORY: { observer: MutationObserver; timer: ReturnType<typeof setTimeout> | null } = {
|
||||||
|
observer: new MutationObserver((_) => {
|
||||||
|
if (DOM_OBSERVATORY.timer) clearTimeout(DOM_OBSERVATORY.timer);
|
||||||
|
DOM_OBSERVATORY.timer = setTimeout(() => {
|
||||||
|
console.debug("observatory trigger");
|
||||||
|
traverse();
|
||||||
|
}, 1000);
|
||||||
|
}),
|
||||||
|
timer: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const VIEWPORT_OBSERVATORY: { observer: IntersectionObserver } = {
|
||||||
|
observer: new IntersectionObserver((entries, observer) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const stack = STATE.stack.get(entry.target);
|
||||||
|
if (stack) {
|
||||||
|
sendMessage("inference_aiText_classify", { text: stack.text })
|
||||||
|
.then((resp) => {
|
||||||
|
console.debug("received inf response", resp);
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
apply_class(entry.target, resp.prob_ai, resp.prob_human);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn("inference failed", e, "\nentry will be rescheduled by observer");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn("viewport_observatory triggered on node, but the stack is absent");
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATE: {
|
||||||
|
stack: WeakMap<Element, { text: string }>;
|
||||||
|
badge: WeakMap<Node, Element>;
|
||||||
|
} = {
|
||||||
|
stack: new WeakMap(),
|
||||||
|
badge: new WeakMap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function setup() {
|
||||||
|
DOM_OBSERVATORY.observer.observe(document.body, {
|
||||||
|
subtree: true,
|
||||||
|
characterData: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function traverse() {
|
||||||
|
CONFIG.sync.then(() => {
|
||||||
|
if (!CONFIG.value.enabled) {
|
||||||
|
console.debug("disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug("traverse start");
|
||||||
|
for (const stack of traverse_text(600, 20)) {
|
||||||
|
const target = stack.first_node.parentElement || stack.root.parentElement;
|
||||||
|
if (target) {
|
||||||
|
set_badge(target);
|
||||||
|
STATE.stack.set(target, { text: stack.text });
|
||||||
|
VIEWPORT_OBSERVATORY.observer.observe(target);
|
||||||
|
} else {
|
||||||
|
console.warn("Stack didn't have target");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply_class(target: Node, prob_ai: number, prob_human: number) {
|
||||||
|
CONFIG.sync.then(() => {
|
||||||
|
if (CONFIG.value.show_ai && prob_ai >= CONFIG.value.threshold && prob_ai >= prob_human) {
|
||||||
|
set_badge(target, { type: "ai", prob: prob_ai });
|
||||||
|
} else if (CONFIG.value.show_human && prob_human >= CONFIG.value.threshold && prob_human >= prob_ai) {
|
||||||
|
set_badge(target, { type: "human", prob: prob_human });
|
||||||
|
} else {
|
||||||
|
clear_badge(target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_badge(target: Node, data?: { type: "ai" | "human"; prob: number }) {
|
||||||
|
if (STATE.badge.has(target)) STATE.badge.delete(target);
|
||||||
|
|
||||||
|
let badge = document.createElement("mark");
|
||||||
|
badge.setAttribute(TRAVERSE_BEHAVOUR_ATTR, TRAVERSE_BEHAVOUR_IGNORE_MAGIC);
|
||||||
|
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 (data?.type === "ai") {
|
||||||
|
badge.style.backgroundColor = "#ffc36d";
|
||||||
|
badge.style.color = "#fff";
|
||||||
|
badge.textContent = `🤖: ${Math.trunc(data.prob * 100)}%`;
|
||||||
|
} else if (data?.type === "human") {
|
||||||
|
badge.style.backgroundColor = "#8fe1a1";
|
||||||
|
badge.style.color = "#fff";
|
||||||
|
badge.textContent = `👨💼: ${Math.trunc(data.prob * 100)}%`;
|
||||||
|
} else {
|
||||||
|
badge.style.backgroundColor = "#ddd";
|
||||||
|
badge.style.color = "#fff";
|
||||||
|
badge.textContent = "🦊 •••• ";
|
||||||
|
}
|
||||||
|
|
||||||
|
badge = target.insertBefore(badge, target.firstChild);
|
||||||
|
STATE.badge.set(target, badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear_badge(target: Node) {
|
||||||
|
STATE.badge.delete(target);
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "./app.css";
|
import "./app.css";
|
||||||
|
|
||||||
import { extensionConfig } from "@/utils/config";
|
import { EXTENSION_CONFIG_STORE } from "@/utils/config";
|
||||||
|
|
||||||
class Config<Key extends keyof typeof extensionConfig> {
|
class Config<Key extends keyof typeof EXTENSION_CONFIG_STORE> {
|
||||||
key: Key;
|
key: Key;
|
||||||
value: any;
|
value: any;
|
||||||
insync = false;
|
insync = false;
|
||||||
|
|
||||||
constructor(key: Key) {
|
constructor(key: Key) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this.value = $state((extensionConfig[key] as any).fallback);
|
this.value = $state((EXTENSION_CONFIG_STORE[key] as any).fallback);
|
||||||
this.load();
|
this.load();
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const value = $state.snapshot(this.value);
|
const value = $state.snapshot(this.value);
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
if (!this.insync || this.value == value) return;
|
if (!this.insync || this.value == value) return;
|
||||||
extensionConfig[this.key].setValue(value).catch((e) => {
|
EXTENSION_CONFIG_STORE[this.key].setValue(value).catch((e) => {
|
||||||
console.error("Failed to write state back to storage", e);
|
console.error("Failed to write state back to storage", e);
|
||||||
});
|
});
|
||||||
}, 20);
|
}, 20);
|
||||||
|
|
@ -27,14 +27,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const config = await extensionConfig[this.key].getValue().catch((e) => {
|
const config = await EXTENSION_CONFIG_STORE[this.key].getValue().catch((e) => {
|
||||||
console.error("Failed to sync state from storage", e);
|
console.error("Failed to sync state from storage", e);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.value = config;
|
this.value = config;
|
||||||
this.insync = true;
|
this.insync = true;
|
||||||
|
|
||||||
(extensionConfig[this.key] as any).watch((value: any) => {
|
(EXTENSION_CONFIG_STORE[this.key] as any).watch((value: any) => {
|
||||||
if (value != this.value) this.value = value;
|
if (value != this.value) this.value = value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -216,7 +216,7 @@
|
||||||
|
|
||||||
<fieldset class="fieldset border-base-300 border rounded-box p-2">
|
<fieldset class="fieldset border-base-300 border rounded-box p-2">
|
||||||
<legend class="fieldset-legend"> Do not check on these sites: </legend>
|
<legend class="fieldset-legend"> Do not check on these sites: </legend>
|
||||||
<div class="flex gap-2 flex-wrap overflow-y-auto max-h-30 w-full">
|
<div class="flex gap-2 flex-wrap overflow-y-auto h-20 max-h-20 w-full">
|
||||||
{#each domain_whitelist.value as domain}
|
{#each domain_whitelist.value as domain}
|
||||||
<div class="badge">
|
<div class="badge">
|
||||||
{domain}
|
{domain}
|
||||||
|
|
@ -228,12 +228,12 @@
|
||||||
<input type="radio" name="cfgtabs0" class="tab" aria-label="🛈" />
|
<input type="radio" name="cfgtabs0" class="tab" aria-label="🛈" />
|
||||||
<div class="tab-content bg-base-100 border-base-300 p-2">
|
<div class="tab-content bg-base-100 border-base-300 p-2">
|
||||||
<p>
|
<p>
|
||||||
Firaid-moz webextension v{browser.runtime.getManifest().version}
|
Firaid-moz v{browser.runtime.getManifest().version}
|
||||||
by Spushk 🦊
|
by Spushk 🦊
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
<p>
|
<p>
|
||||||
The source code is available <a class="link" href={browser.runtime.getManifest().homepage_url}
|
Source code is available <a class="link" href={browser.runtime.getManifest().homepage_url}
|
||||||
>here</a
|
>here</a
|
||||||
> under terms of Apache-2.0 license
|
> under terms of Apache-2.0 license
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
5
extension/src/entrypoints/popup/ConfigImage.svelte
Normal file
5
extension/src/entrypoints/popup/ConfigImage.svelte
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import { Config } from "./config.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section></section>
|
||||||
3
extension/src/entrypoints/popup/ConfigText.svelte
Normal file
3
extension/src/entrypoints/popup/ConfigText.svelte
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<script></script>
|
||||||
|
|
||||||
|
<section></section>
|
||||||
5
extension/src/entrypoints/popup/General.svelte
Normal file
5
extension/src/entrypoints/popup/General.svelte
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import { Config } from "./config.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section></section>
|
||||||
40
extension/src/entrypoints/popup/config.svelte.ts
Normal file
40
extension/src/entrypoints/popup/config.svelte.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { EXTENSION_CONFIG_STORE } from "@/utils/config";
|
||||||
|
|
||||||
|
export class Config<Key extends keyof typeof EXTENSION_CONFIG_STORE> {
|
||||||
|
key: Key;
|
||||||
|
value: (typeof EXTENSION_CONFIG_STORE)[Key]["fallback"];
|
||||||
|
insync = false;
|
||||||
|
|
||||||
|
constructor(key: Key) {
|
||||||
|
this.key = key;
|
||||||
|
this.value = $state(EXTENSION_CONFIG_STORE[this.key].fallback);
|
||||||
|
|
||||||
|
EXTENSION_CONFIG_STORE[this.key]
|
||||||
|
.getValue()
|
||||||
|
.then((value) => {
|
||||||
|
this.value = value;
|
||||||
|
this.insync = true;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Failed to sync store config into popup", e);
|
||||||
|
});
|
||||||
|
|
||||||
|
EXTENSION_CONFIG_STORE[this.key].watch((value) => {
|
||||||
|
if (value != this.value) this.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const value = $state.snapshot(this.value);
|
||||||
|
|
||||||
|
if (!this.insync) return;
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
EXTENSION_CONFIG_STORE[this.key].setValue(value as any).catch((e) => {
|
||||||
|
console.error("Failed to sync popup config into store", e);
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ export interface ExtensionConfigAiImage {
|
||||||
threshold: number;
|
threshold: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extensionConfig = {
|
export const EXTENSION_CONFIG_STORE = {
|
||||||
domain_whitelist: storage.defineItem("local:config:domain_whitelist", {
|
domain_whitelist: storage.defineItem("local:config:domain_whitelist", {
|
||||||
fallback: [],
|
fallback: [],
|
||||||
init: () => ["asd.asd", "lkj.asd", "oiua.asd", "oi1u2e.aoisdu", "did0.asdiho"],
|
init: () => ["asd.asd", "lkj.asd", "oiua.asd", "oi1u2e.aoisdu", "did0.asdiho"],
|
||||||
|
|
@ -59,36 +59,57 @@ export const extensionConfig = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EXTENSION_CONFIG: {
|
export class SyncingExtensionConfig<Key extends keyof typeof EXTENSION_CONFIG_STORE> {
|
||||||
domain_whitelist: Set<string>;
|
#key: Key;
|
||||||
ai_text: ExtensionConfigAiText;
|
#value: (typeof EXTENSION_CONFIG_STORE)[Key]["fallback"];
|
||||||
ai_image: ExtensionConfigAiImage;
|
#sync: Promise<void>;
|
||||||
} = {
|
#sync_resolve = () => {};
|
||||||
domain_whitelist: new Set(extensionConfig.domain_whitelist.fallback),
|
#sync_reject = () => {};
|
||||||
ai_text: extensionConfig.ai_text.fallback,
|
|
||||||
ai_image: extensionConfig.ai_image.fallback,
|
|
||||||
};
|
|
||||||
|
|
||||||
extensionConfig.domain_whitelist.getValue().then((config) => {
|
constructor(key: Key) {
|
||||||
EXTENSION_CONFIG.domain_whitelist = new Set(config);
|
this.#key = key;
|
||||||
|
this.#value = EXTENSION_CONFIG_STORE[key].fallback;
|
||||||
extensionConfig.domain_whitelist.watch((config) => {
|
this.#sync = new Promise((res, rej) => {
|
||||||
EXTENSION_CONFIG.domain_whitelist = new Set(config);
|
this.#sync_resolve = res;
|
||||||
|
this.#sync_reject = rej;
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
extensionConfig.ai_text.getValue().then((config) => {
|
EXTENSION_CONFIG_STORE[key].getValue().then((v) => {
|
||||||
EXTENSION_CONFIG.ai_text = config;
|
this.#value = v;
|
||||||
|
this.#sync_resolve();
|
||||||
extensionConfig.ai_text.watch((config) => {
|
|
||||||
EXTENSION_CONFIG.ai_text = config;
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
extensionConfig.ai_image.getValue().then((config) => {
|
EXTENSION_CONFIG_STORE[key].watch((v) => {
|
||||||
EXTENSION_CONFIG.ai_image = config;
|
this.#value = v;
|
||||||
|
|
||||||
extensionConfig.ai_image.watch((config) => {
|
|
||||||
EXTENSION_CONFIG.ai_image = config;
|
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
callback: (
|
||||||
|
new_value: (typeof EXTENSION_CONFIG_STORE)[Key]["fallback"],
|
||||||
|
old_value: (typeof EXTENSION_CONFIG_STORE)[Key]["fallback"],
|
||||||
|
) => void,
|
||||||
|
) {
|
||||||
|
EXTENSION_CONFIG_STORE[this.#key].watch(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
get sync() {
|
||||||
|
return this.#sync;
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.#value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(value) {
|
||||||
|
this.#value = value;
|
||||||
|
this.#sync = new Promise((res, rej) => {
|
||||||
|
this.#sync_resolve = res;
|
||||||
|
this.#sync_reject = rej;
|
||||||
|
});
|
||||||
|
|
||||||
|
EXTENSION_CONFIG_STORE[this.#key].setValue(value as any).then(() => {
|
||||||
|
this.#sync_resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,17 @@ const KNOWN_STACK_ROOTS: WeakMap<
|
||||||
}
|
}
|
||||||
> = new WeakMap();
|
> = new WeakMap();
|
||||||
|
|
||||||
export function* traverse_text(soft_max_words: number, hard_min_words: number) {
|
export type TraverseTextStack = {
|
||||||
|
root: Node;
|
||||||
|
first_node: Node;
|
||||||
|
text: string;
|
||||||
|
words: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function* traverse_text(
|
||||||
|
soft_max_words: number,
|
||||||
|
hard_min_words: number,
|
||||||
|
): Generator<TraverseTextStack, void, void> {
|
||||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
|
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
|
||||||
acceptNode: (node) => {
|
acceptNode: (node) => {
|
||||||
// sanity check
|
// sanity check
|
||||||
|
|
@ -112,12 +122,12 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// drop with nonsense content
|
// drop with nonsense content
|
||||||
// if (!node.textContent?.trim()) {
|
if (!node.textContent?.trim()) {
|
||||||
// if (IS_DEV) {
|
if (IS_DEV) {
|
||||||
// node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-NO_TEXT");
|
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-NO_TEXT");
|
||||||
// }
|
}
|
||||||
// return NodeFilter.FILTER_REJECT;
|
return NodeFilter.FILTER_REJECT;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// finally good ones
|
// finally good ones
|
||||||
if (IS_DEV) {
|
if (IS_DEV) {
|
||||||
|
|
@ -127,7 +137,7 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let stack: { root: Node; first_node: Node; last_node: Node; text: string; words: number } | null = null;
|
let stack: TraverseTextStack | null = null;
|
||||||
|
|
||||||
while (walker.nextNode()) {
|
while (walker.nextNode()) {
|
||||||
const node = walker.currentNode;
|
const node = walker.currentNode;
|
||||||
|
|
@ -154,14 +164,12 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
|
||||||
stack = {
|
stack = {
|
||||||
root: rootable_parent,
|
root: rootable_parent,
|
||||||
first_node: node,
|
first_node: node,
|
||||||
last_node: node,
|
|
||||||
text: nodeText,
|
text: nodeText,
|
||||||
words: nodeTextWords,
|
words: nodeTextWords,
|
||||||
};
|
};
|
||||||
} else if (stack.root.contains(rootable_parent)) {
|
} else if (stack.root.contains(rootable_parent)) {
|
||||||
stack.text += " " + nodeText;
|
stack.text += " " + nodeText;
|
||||||
stack.words += nodeTextWords;
|
stack.words += nodeTextWords;
|
||||||
stack.last_node = node;
|
|
||||||
} else {
|
} else {
|
||||||
if (stack.words >= hard_min_words) {
|
if (stack.words >= hard_min_words) {
|
||||||
const known = KNOWN_STACK_ROOTS.get(stack.root);
|
const known = KNOWN_STACK_ROOTS.get(stack.root);
|
||||||
|
|
@ -176,7 +184,6 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
|
||||||
stack = {
|
stack = {
|
||||||
root: rootable_parent,
|
root: rootable_parent,
|
||||||
first_node: node,
|
first_node: node,
|
||||||
last_node: node,
|
|
||||||
text: nodeText,
|
text: nodeText,
|
||||||
words: nodeTextWords,
|
words: nodeTextWords,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,44 @@ export enum ExtLogLevel {
|
||||||
export const ExtConsole = (sender: string) => {
|
export const ExtConsole = (sender: string) => {
|
||||||
return {
|
return {
|
||||||
debug(...args: any[]) {
|
debug(...args: any[]) {
|
||||||
sendMessage("log", { level: ExtLogLevel.Debug, sender, args });
|
sendMessage("log", { level: ExtLogLevel.Debug, sender, args }).catch((e) => {
|
||||||
|
console.debug(
|
||||||
|
"[firaid-moz] (sorry to mess with your site logs): ERROR: can not send message through ExtConsole:",
|
||||||
|
e,
|
||||||
|
`\n original message was from [${sender}]:`,
|
||||||
|
...args,
|
||||||
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
log(...args: any[]) {
|
log(...args: any[]) {
|
||||||
sendMessage("log", { level: ExtLogLevel.Info, sender, args });
|
sendMessage("log", { level: ExtLogLevel.Info, sender, args }).catch((e) => {
|
||||||
|
console.debug(
|
||||||
|
"[firaid-moz] (sorry to mess with your site logs): ERROR: can not send message through ExtConsole:",
|
||||||
|
e,
|
||||||
|
`\n original message was from [${sender}]:`,
|
||||||
|
...args,
|
||||||
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
warn(...args: any[]) {
|
warn(...args: any[]) {
|
||||||
sendMessage("log", { level: ExtLogLevel.Warning, sender, args });
|
sendMessage("log", { level: ExtLogLevel.Warning, sender, args }).catch((e) => {
|
||||||
|
console.debug(
|
||||||
|
"[firaid-moz] (sorry to mess with your site logs): ERROR: can not send message through ExtConsole:",
|
||||||
|
e,
|
||||||
|
`\n original message was from [${sender}]:`,
|
||||||
|
...args,
|
||||||
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
error(...args: any[]) {
|
error(...args: any[]) {
|
||||||
sendMessage("log", { level: ExtLogLevel.Error, sender, args });
|
sendMessage("log", { level: ExtLogLevel.Error, sender, args }).catch((e) => {
|
||||||
|
console.debug(
|
||||||
|
"[firaid-moz] (sorry to mess with your site logs): ERROR: can not send message through ExtConsole:",
|
||||||
|
e,
|
||||||
|
`\n original message was from [${sender}]:`,
|
||||||
|
...args,
|
||||||
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
16
extension/src/utils/system.ts
Normal file
16
extension/src/utils/system.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
export async function has_wgpu() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
"extends": "./.wxt/tsconfig.json",
|
"extends": "./.wxt/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
},
|
},
|
||||||
"files": [],
|
"files": [],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue