Still WIP
This commit is contained in:
parent
2109aaa16d
commit
6d28a2f53a
16 changed files with 690 additions and 383 deletions
|
|
@ -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">
|
||||
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>
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
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",
|
||||
"compilerOptions": {
|
||||
"useDefineForClassFields": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
},
|
||||
"files": [],
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue