Still WIP

This commit is contained in:
spushk 2026-05-13 14:08:53 +05:00 • committed by spushk
commit 6d28a2f53a
Signed by: spushk
GPG key ID: 7797B761E105E8AA
16 changed files with 690 additions and 383 deletions

View file

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

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

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

View file

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

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

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

View file

@ -1,23 +1,23 @@
<script lang="ts">
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;
value: any;
insync = false;
constructor(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();
$effect(() => {
const value = $state.snapshot(this.value);
const timeout = setTimeout(() => {
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);
});
}, 20);
@ -27,14 +27,14 @@
}
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);
});
this.value = config;
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;
});
}
@ -216,7 +216,7 @@
<fieldset class="fieldset border-base-300 border rounded-box p-2">
<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}
<div class="badge">
{domain}
@ -228,12 +228,12 @@
<input type="radio" name="cfgtabs0" class="tab" aria-label="🛈" />
<div class="tab-content bg-base-100 border-base-300 p-2">
<p>
Firaid-moz webextension v{browser.runtime.getManifest().version}
Firaid-moz v{browser.runtime.getManifest().version}
by Spushk 🦊
</p>
<br />
<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
> under terms of Apache-2.0 license
</p>

View file

@ -0,0 +1,5 @@
<script>
import { Config } from "./config.svelte";
</script>
<section></section>

View file

@ -0,0 +1,3 @@
<script></script>
<section></section>

View file

@ -0,0 +1,5 @@
<script>
import { Config } from "./config.svelte";
</script>
<section></section>

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

View file

@ -14,7 +14,7 @@ export interface ExtensionConfigAiImage {
threshold: number;
}
export const extensionConfig = {
export const EXTENSION_CONFIG_STORE = {
domain_whitelist: storage.defineItem("local:config:domain_whitelist", {
fallback: [],
init: () => ["asd.asd", "lkj.asd", "oiua.asd", "oi1u2e.aoisdu", "did0.asdiho"],
@ -59,36 +59,57 @@ export const extensionConfig = {
}),
};
export const EXTENSION_CONFIG: {
domain_whitelist: Set<string>;
ai_text: ExtensionConfigAiText;
ai_image: ExtensionConfigAiImage;
} = {
domain_whitelist: new Set(extensionConfig.domain_whitelist.fallback),
ai_text: extensionConfig.ai_text.fallback,
ai_image: extensionConfig.ai_image.fallback,
};
export class SyncingExtensionConfig<Key extends keyof typeof EXTENSION_CONFIG_STORE> {
#key: Key;
#value: (typeof EXTENSION_CONFIG_STORE)[Key]["fallback"];
#sync: Promise<void>;
#sync_resolve = () => {};
#sync_reject = () => {};
extensionConfig.domain_whitelist.getValue().then((config) => {
EXTENSION_CONFIG.domain_whitelist = new Set(config);
constructor(key: Key) {
this.#key = key;
this.#value = EXTENSION_CONFIG_STORE[key].fallback;
this.#sync = new Promise((res, rej) => {
this.#sync_resolve = res;
this.#sync_reject = rej;
});
extensionConfig.domain_whitelist.watch((config) => {
EXTENSION_CONFIG.domain_whitelist = new Set(config);
});
});
EXTENSION_CONFIG_STORE[key].getValue().then((v) => {
this.#value = v;
this.#sync_resolve();
});
extensionConfig.ai_text.getValue().then((config) => {
EXTENSION_CONFIG.ai_text = config;
EXTENSION_CONFIG_STORE[key].watch((v) => {
this.#value = v;
});
}
extensionConfig.ai_text.watch((config) => {
EXTENSION_CONFIG.ai_text = 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);
}
extensionConfig.ai_image.getValue().then((config) => {
EXTENSION_CONFIG.ai_image = config;
get sync() {
return this.#sync;
}
extensionConfig.ai_image.watch((config) => {
EXTENSION_CONFIG.ai_image = config;
});
});
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();
});
}
}

View file

@ -48,7 +48,17 @@ const KNOWN_STACK_ROOTS: 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, {
acceptNode: (node) => {
// sanity check
@ -112,12 +122,12 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
}
// drop with nonsense content
// if (!node.textContent?.trim()) {
// if (IS_DEV) {
// node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-NO_TEXT");
// }
// return NodeFilter.FILTER_REJECT;
// }
if (!node.textContent?.trim()) {
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-NO_TEXT");
}
return NodeFilter.FILTER_REJECT;
}
// finally good ones
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()) {
const node = walker.currentNode;
@ -154,14 +164,12 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
stack = {
root: rootable_parent,
first_node: node,
last_node: node,
text: nodeText,
words: nodeTextWords,
};
} else if (stack.root.contains(rootable_parent)) {
stack.text += " " + nodeText;
stack.words += nodeTextWords;
stack.last_node = node;
} else {
if (stack.words >= hard_min_words) {
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 = {
root: rootable_parent,
first_node: node,
last_node: node,
text: nodeText,
words: nodeTextWords,
};

View file

@ -10,16 +10,44 @@ export enum ExtLogLevel {
export const ExtConsole = (sender: string) => {
return {
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[]) {
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[]) {
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[]) {
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,
);
});
},
};
};

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

View file

@ -2,6 +2,8 @@
"extends": "./.wxt/tsconfig.json",
"compilerOptions": {
"useDefineForClassFields": true,
"strict": true,
"strictNullChecks": true,
},
"files": [],
}