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