From 914bcbb474f6f186c212b2da0d9d864b5e75d8e4 Mon Sep 17 00:00:00 2001 From: RaindropsSys Date: Sat, 29 Jun 2024 20:59:16 +0200 Subject: Rename to Faunerie --- src/FaunerieAI.ts | 224 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100755 src/FaunerieAI.ts (limited to 'src/FaunerieAI.ts') diff --git a/src/FaunerieAI.ts b/src/FaunerieAI.ts new file mode 100755 index 0000000..aa93cbb --- /dev/null +++ b/src/FaunerieAI.ts @@ -0,0 +1,224 @@ +import {FaunerieApp} from "./FaunerieApp"; +import {ChildProcess} from "node:child_process"; +import {FaunerieImageType} from "libfaunerie"; +import * as fs from "node:fs"; +import * as cp from "node:child_process"; + +export class FaunerieAI { + instance: FaunerieApp; + aiProcess: ChildProcess; + aiStarting: boolean; + + constructor(instance: FaunerieApp) { + this.instance = instance; + } + + get aiRunning() { + return (!!this.aiProcess && this.aiProcess.exitCode === null) || this.aiStarting; + } + + findPythonExecutable() { + try { + if (process.platform !== "darwin") throw new Error("Not running on macOS"); + cp.execFileSync("/usr/local/bin/python3.11", ["--version"], {cwd: __dirname + "/../ai"}).toString().trim(); + return "/usr/local/bin/python3.11"; + } catch (e) { + try { + cp.execFileSync("py", ["--version"], {cwd: __dirname + "/../ai"}).toString().trim(); + return "py"; + } catch (e) { + try { + cp.execFileSync("python3.11", ["--version"], {cwd: __dirname + "/../ai"}).toString().trim(); + return "python3.11"; + } catch (e) { + try { + cp.execFileSync("python3", ["--version"], {cwd: __dirname + "/../ai"}).toString().trim(); + return "python3"; + } catch (e) { + cp.execFileSync("python", ["--version"], {cwd: __dirname + "/../ai"}).toString().trim(); + return "python"; + } + } + } + } + } + + getPythonVersion() { + return cp.execFileSync(this.findPythonExecutable(), ["--version"], {cwd: __dirname + "/../ai"}).toString().trim(); + } + + updateDependencies() { + return new Promise((res) => { + this.aiProcess = cp.execFile(this.findPythonExecutable(), ["-m", "pip", "install", "-U", "torch", "ultralytics", "Pillow", "requests", "pandas", "opencv-python", "flask", "gitpython", "setuptools>=65.5.1"], {cwd: __dirname + "/../ai"}); + + this.aiProcess.stdout.on('data', (d) => { + console.debug(d.toString()); + }); + + this.aiProcess.stderr.on('data', (d) => { + console.debug(d.toString()); + }); + + this.aiProcess.on('exit', () => { + res(); + }); + }); + } + + triggerEngineStart() { + this.aiProcess = cp.execFile(this.findPythonExecutable(), ["server.py"], {cwd: __dirname + "/../ai"}); + + this.aiProcess.stdout.on('data', (d) => { + console.debug(d.toString()); + }); + + this.aiProcess.stderr.on('data', (d) => { + console.debug(d.toString()); + }); + + this.aiProcess.on('exit', () => { + this.aiProcess = null; + }); + } + + waitForEngine() { + return new Promise((res) => { + let aiInterval = setInterval(async () => { + try { + if ("data" in (await (await fetch("http://127.0.0.1:25091/status")).json())) { + clearInterval(aiInterval); + this.aiStarting = false; + res(); + } + } catch (e) { + } + }, 1000); + }) + } + + validatePythonVersion() { + let pyVersion: string = this.getPythonVersion(); + + if (!pyVersion) { + console.log("Unable to find a Python executable"); + throw new Error("Python not found"); + } else if (!pyVersion.startsWith("Python 3.11.") && pyVersion !== "Python 3.11") { + console.log("Invalid Python version: " + pyVersion); + throw new Error("Python not found"); + } + } + + async startAI() { + if (this.aiRunning) return; + + this.aiStarting = true; + this.validatePythonVersion(); + await this.updateDependencies(); + this.triggerEngineStart(); + await this.waitForEngine(); + } + + async getClasses() { + let _dataStore = this.instance.dataStore; + let protectedDecode = (b: Buffer) => { + return require('zlib').inflateRawSync(b); + } + + await this.startAI(); + let url = _dataStore.database.frontend.getImageFile(_dataStore.currentImage, FaunerieImageType.ViewURL); + let data: any; + + if (url.startsWith("blob:") || url.startsWith("pbip:")) { + fs.writeFileSync(_dataStore.appData + "/.temp", protectedDecode(fs.readFileSync(_dataStore.database.frontend.getImageFile(_dataStore.currentImage, FaunerieImageType.ViewFile)))); + url = "file://" + (_dataStore.appData + "/.temp").replaceAll("\\", "/"); + } + + if (_dataStore.currentImage.tags.includes("safe")) { + data = await (await fetch("http://127.0.0.1:25091/safe?url=" + encodeURIComponent(url.replace("file://", "")))).json(); + } else { + data = await (await fetch("http://127.0.0.1:25091/explicit?url=" + encodeURIComponent(url.replace("file://", "")))).json(); + } + + if (fs.existsSync(_dataStore.appData + "/.temp")) fs.unlinkSync(_dataStore.appData + "/.temp"); + return data; + } + + unload() { + if (this.instance.dataStore.appData && fs.existsSync(this.instance.dataStore.appData + "/.temp")) fs.unlinkSync(this.instance.dataStore.appData + "/.temp"); + + if (this.aiProcess) { + console.log("Engine did not stop before quitting, forcefully killing it"); + this.aiProcess.kill("SIGKILL"); + } + } + + makeClassesHTML(c: any) { + let _dataStore = this.instance.dataStore; + + window.onresize = () => { + document.getElementById("preview-zones").style.top = document.getElementById("preview-content-inner").offsetTop + "px"; + document.getElementById("preview-zones").style.left = document.getElementById("preview-content-inner").offsetLeft + "px"; + document.getElementById("preview-zones").style.width = document.getElementById("preview-content-inner").clientWidth + "px"; + document.getElementById("preview-zones").style.height = document.getElementById("preview-content-inner").clientHeight + "px"; + } + + window.onresize(null); + _dataStore.currentImageClasses = c.data || []; + + let properClasses = _dataStore.currentImageClasses.filter(i => i.confidence > 0.5).map(i => { + return { + name: i.name, + top: (i.ymin / _dataStore.currentImage.height) * 100, + left: (i.xmin / _dataStore.currentImage.width) * 100, + width: ((i.xmax - i.xmin) / _dataStore.currentImage.width) * 100, + height: ((i.ymax - i.ymin) / _dataStore.currentImage.height) * 100 + } + }); + document.getElementById("preview-zones").innerHTML = properClasses.map((i, j) => ` +
+ `).join(""); + + this.displayClassesList(properClasses); + } + + displayClassesList(properClasses: any) { + if (properClasses.length === 0) { + document.getElementById("preview-parts-loader").style.display = "none"; + document.getElementById("preview-parts-none").style.display = ""; + document.getElementById("preview-parts-unsupported").style.display = "none"; + document.getElementById("preview-parts-list").style.display = "none"; + } else { + document.getElementById("preview-parts-loader").style.display = "none"; + document.getElementById("preview-parts-none").style.display = "none"; + document.getElementById("preview-parts-unsupported").style.display = "none"; + document.getElementById("preview-parts-list").style.display = ""; + document.getElementById("preview-parts-list").innerHTML = properClasses.map((i, j) => ` + ${i.name} + `).join(""); + } + } + + displayClasses(id: string) { + let _dataStore = this.instance.dataStore; + + if (_dataStore.currentImage.mime_type.startsWith("image/")) { + if (_dataStore.currentImage.tags.includes("screencap") || !_dataStore.currentImage.tags.includes("safe")) { + this.instance.ai.getClasses().then((c: any) => { + if (_dataStore.currentImage.id === parseInt(id)) { + this.makeClassesHTML(c); + } + }); + } else { + document.getElementById("preview-parts-loader").style.display = "none"; + document.getElementById("preview-parts-none").style.display = ""; + document.getElementById("preview-parts-unsupported").style.display = "none"; + document.getElementById("preview-parts-list").style.display = "none"; + } + } else { + document.getElementById("preview-parts-loader").style.display = "none"; + document.getElementById("preview-parts-none").style.display = "none"; + document.getElementById("preview-parts-unsupported").style.display = ""; + document.getElementById("preview-parts-list").style.display = "none"; + } + } +} -- cgit