Merge remote-tracking branch 'origin/linux' into linux

# Conflicts:
#	scripts/serve.cjs
This commit is contained in:
orailnoor committed 2026-06-14 08:36:19 +05:30
commit b55e62be7d
24 files changed
+1474 -179

No files matched your search

+1
View File
@@ -0,0 +1 @@
*.sh text eol=lf
+6 -5
View File
@@ -3,11 +3,17 @@ app/models/*.safetensors
app/models/*.gguf
app/models/*.ckpt
app/models
app/openvino-models/
app/outputs/
# Ignore generated frontend build output
app/dist/
# Ignore portable Node.js binaries and packages
app/tools/
**/node_modules/
**/node_modules_mac/
**/node_modules_linux/
node_modules/
# Ignore backend folder completely (contains massive DLLs, EXEs, and ZIPs >1GB)
@@ -20,8 +26,3 @@ app/backend/
Thumbs.db
*.log
*.tmp
# output images folder
/app/outputs
+17 -11
View File
@@ -22,27 +22,28 @@
---
## 📖 Overview
**Local AI Image Generator** is a zero-configuration, portable desktop environment for running Stable Diffusion (Safetensors/GGUF/CKPT) offline on Windows, Linux, and macOS. Double-clicking `start.bat` (Windows) or running `./start.sh` (Linux/macOS) automatically handles dependency setup, GPU backend matching, and launches a high-performance local web workspace.
**Local AI Image Generator** is a zero-configuration, portable desktop environment for running Stable Diffusion (Safetensors/GGUF/CKPT) offline on Windows, Linux, and macOS. Running `windows.bat` (Windows), `./linux.sh` (Linux), or `./mac.sh` (macOS) automatically handles dependency setup, GPU backend matching, and launches a high-performance local web workspace.
---
## ⚡ Quick Start
### Windows
1. **Launch:** Double-click **`start.bat`** (downloads portable Node.js and pre-compiled GPU backend binaries on first run).
1. **Launch:** Double-click **`windows.bat`** (downloads portable Node.js and pre-compiled GPU backend binaries on first run).
2. **Add Models:** Drop `.safetensors`, `.gguf`, or `.ckpt` weights into `app/models/` (or download them via the **Model Manager** tab in the UI).
3. **Generate:** Open `http://localhost:1420` in your browser, select your model, and write a prompt.
### Linux
1. **Check compatibility:** Prebuilt Linux backends are built on Ubuntu 24.04 and require **glibc 2.38+**. Run `ldd --version` to verify.
2. **Launch:** Open a terminal in the project folder and run **`./start.sh`** (downloads portable Node.js and pre-compiled GPU backend binaries on first run).
- For maximum AMD GPU performance, use **`./start.sh --max-perf`** on first setup (adds the ROCm backend).
2. **Launch:** Open a terminal in the project folder and run **`./linux.sh`** (downloads portable Node.js and pre-compiled GPU backend binaries on first run).
- For maximum AMD GPU performance, use **`./linux.sh --max-perf`** on first setup (adds the ROCm backend).
- For Intel Core Ultra NPU support, install the Intel Linux NPU driver, then run **`./linux.sh --setup-openvino`**.
3. **Add Models:** Drop `.safetensors`, `.gguf`, or `.ckpt` weights into `app/models/` (or download them via the **Model Manager** tab in the UI).
4. **Generate:** Open `http://localhost:1420` in your browser, select your model, and write a prompt.
### macOS
1. **Check compatibility:** The prebuilt macOS backend is for **Apple Silicon (M1 or newer)** and uses **Metal** GPU acceleration.
2. **Launch:** Open Terminal in the project folder and run **`./start.sh`** (downloads portable Node.js and the pre-compiled Metal backend on first run).
2. **Launch:** Open Terminal in the project folder and run **`./mac.sh`** (downloads portable Node.js and the pre-compiled Metal backend on first run).
3. **Add Models:** Drop `.safetensors`, `.gguf`, or `.ckpt` weights into `app/models/` (or download them via the **Model Manager** tab in the UI).
4. **Generate:** Open `http://localhost:1420` in your browser, select your model, and write a prompt.
@@ -61,8 +62,9 @@
## 📁 Repository Structure
```
local-ai-image-generator/
├── start.bat # Main double-click entrypoint (Windows)
├── start.sh # Main terminal entrypoint (Linux/macOS)
├── windows.bat # Main double-click entrypoint (Windows)
├── linux.sh # Main terminal entrypoint (Linux)
├── mac.sh # Main terminal entrypoint (macOS)
├── PLAN.md # Linux port implementation plan
├── LICENSE # MIT Open Source license
├── .gitignore
@@ -99,6 +101,7 @@ local-ai-image-generator/
| **NVIDIA** | Vulkan | CPU | No reliable prebuilt Linux CUDA binary is currently available. NVIDIA GPUs use Vulkan. |
| **AMD Radeon** | ROCm | Vulkan | ROCm provides best AMD performance when host ROCm drivers are available. |
| **Intel Arc / integrated** | Vulkan | CPU | Cross-vendor Vulkan support. |
| **Intel Core Ultra NPU** | OpenVINO NPU | CPU | Requires the Intel Linux NPU driver, kernel 6.6+, Python 3, and `./linux.sh --setup-openvino`. |
| **Integrated / None** | CPU | — | Runs on logical CPU threads (slow). |
### macOS
@@ -109,13 +112,16 @@ local-ai-image-generator/
| **Intel Mac** | Source build required | CPU | Official prebuilt macOS backend is Apple Silicon only. |
**System requirements:**
- **64-bit Windows 10 or Windows 11** is required for the portable Node.js 22 runtime used by the Windows launcher.
- **glibc 2.38 or newer** is required for the prebuilt Linux backends (Ubuntu 24.04, Fedora 40+, etc.).
- The setup script will warn you if your glibc is older. You can still run setup, but the prebuilt backends will not start.
- **Linux OpenVINO NPU:** Intel Core Ultra, x86_64 Linux, kernel 6.6+, a working `/dev/accel/accel0` device, Python 3 with `venv`, and the Intel Linux NPU driver are required.
- **Apple Silicon (M1 or newer)** is required for the prebuilt macOS Metal backend.
**Linux setup modes:**
- **Default (`./start.sh`)**: Downloads CPU + Vulkan backends (~120–150 MB).
- **Maximum Performance (`./start.sh --max-perf`)**: Also downloads the ROCm backend. Total download ~1.3 GB.
- **Default (`./linux.sh`)**: Downloads CPU + Vulkan backends (~120–150 MB).
- **Maximum Performance (`./linux.sh --max-perf`)**: Also downloads the ROCm backend. Total download ~1.3 GB.
- **Intel NPU (`./linux.sh --setup-openvino`)**: Creates a local OpenVINO Python environment and verifies that the Intel NPU driver exposes an `NPU` device.
---
@@ -137,7 +143,7 @@ Typical generation times for an image with **20 steps** (e.g. 512x512 resolution
* **Linux backends fail to start with `GLIBC_2.38' not found`:** The prebuilt binaries require glibc 2.38+ (Ubuntu 24.04). Upgrade your distribution or build stable-diffusion.cpp from source (see below).
* **Linux ROCm not loading:** Make sure your AMD GPU and kernel are compatible with ROCm 7.13. The app will automatically fall back to Vulkan if ROCm cannot initialize.
* **Windows exits with code `3221225781`:** This is `0xC0000135`, which means Windows could not load a required backend DLL. For AMD/Intel Vulkan, update the GPU driver with Vulkan support, then rerun setup so `app/backend/win/vulkan/` is repaired. For NVIDIA CUDA, update the NVIDIA driver and rerun setup so CUDA runtime DLLs are restored.
* **Generation shows "server is not responding or crashed":** The backend process exited. Check the terminal where you ran `./start.sh` for the exact error (common causes are glibc mismatch, missing Vulkan drivers, or out-of-memory).
* **Generation shows \"server is not responding or crashed\":** The backend process exited. Check the terminal where you ran `./linux.sh` or `./mac.sh` for the exact error (common causes are glibc mismatch, missing Vulkan drivers, or out-of-memory).
---
@@ -190,7 +196,7 @@ After copying, rename the server binary to match what `scripts/serve.cjs` expect
- Vulkan: `sd` → `sd-vulkan`
- ROCm: `sd` → `sd-rocm`
Then restart the app with `./start.sh`.
Then restart the app with `./linux.sh` (Linux) or `./mac.sh` (macOS).
---
-1
View File
@@ -1 +0,0 @@
import{i as D,t as i}from"./index-CG3WZqfX.js";var _;(function(r){r.WINDOW_RESIZED="tauri://resize",r.WINDOW_MOVED="tauri://move",r.WINDOW_CLOSE_REQUESTED="tauri://close-requested",r.WINDOW_DESTROYED="tauri://destroyed",r.WINDOW_FOCUS="tauri://focus",r.WINDOW_BLUR="tauri://blur",r.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",r.WINDOW_THEME_CHANGED="tauri://theme-changed",r.WINDOW_CREATED="tauri://window-created",r.WINDOW_SUSPENDED="tauri://suspended",r.WINDOW_RESUMED="tauri://resumed",r.WEBVIEW_CREATED="tauri://webview-created",r.DRAG_ENTER="tauri://drag-enter",r.DRAG_OVER="tauri://drag-over",r.DRAG_DROP="tauri://drag-drop",r.DRAG_LEAVE="tauri://drag-leave"})(_||(_={}));async function l(r,a){window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(r,a),await D("plugin:event|unlisten",{event:r,eventId:a})}async function n(r,a,e){var t;const W=typeof e?.target=="string"?{kind:"AnyLabel",label:e.target}:(t=e?.target)!==null&&t!==void 0?t:{kind:"Any"};return D("plugin:event|listen",{event:r,target:W,handler:i(a)}).then(d=>async()=>l(r,d))}export{_ as TauriEvent,n as listen};
-23
View File
@@ -1,23 +0,0 @@
(function(){const b=document.createElement("link").relList;if(b&&b.supports&&b.supports("modulepreload"))return;for(const x of document.querySelectorAll('link[rel="modulepreload"]'))r(x);new MutationObserver(x=>{for(const E of x)if(E.type==="childList")for(const q of E.addedNodes)q.tagName==="LINK"&&q.rel==="modulepreload"&&r(q)}).observe(document,{childList:!0,subtree:!0});function N(x){const E={};return x.integrity&&(E.integrity=x.integrity),x.referrerPolicy&&(E.referrerPolicy=x.referrerPolicy),x.crossOrigin==="use-credentials"?E.credentials="include":x.crossOrigin==="anonymous"?E.credentials="omit":E.credentials="same-origin",E}function r(x){if(x.ep)return;x.ep=!0;const E=N(x);fetch(x.href,E)}})();function fm(f){return f&&f.__esModule&&Object.prototype.hasOwnProperty.call(f,"default")?f.default:f}var Rs={exports:{}},ai={};var Zd;function Ny(){if(Zd)return ai;Zd=1;var f=Symbol.for("react.transitional.element"),b=Symbol.for("react.fragment");function N(r,x,E){var q=null;if(E!==void 0&&(q=""+E),x.key!==void 0&&(q=""+x.key),"key"in x){E={};for(var H in x)H!=="key"&&(E[H]=x[H])}else E=x;return x=E.ref,{$$typeof:f,type:r,key:q,ref:x!==void 0?x:null,props:E}}return ai.Fragment=b,ai.jsx=N,ai.jsxs=N,ai}var Kd;function Ey(){return Kd||(Kd=1,Rs.exports=Ny()),Rs.exports}var c=Ey(),Bs={exports:{}},le={};var Jd;function Ay(){if(Jd)return le;Jd=1;var f=Symbol.for("react.transitional.element"),b=Symbol.for("react.portal"),N=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),x=Symbol.for("react.profiler"),E=Symbol.for("react.consumer"),q=Symbol.for("react.context"),H=Symbol.for("react.forward_ref"),w=Symbol.for("react.suspense"),h=Symbol.for("react.memo"),A=Symbol.for("react.lazy"),O=Symbol.for("react.activity"),Q=Symbol.iterator;function P(d){return d===null||typeof d!="object"?null:(d=Q&&d[Q]||d["@@iterator"],typeof d=="function"?d:null)}var J={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},$=Object.assign,fe={};function te(d,j,R){this.props=d,this.context=j,this.refs=fe,this.updater=R||J}te.prototype.isReactComponent={},te.prototype.setState=function(d,j){if(typeof d!="object"&&typeof d!="function"&&d!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,d,j,"setState")},te.prototype.forceUpdate=function(d){this.updater.enqueueForceUpdate(this,d,"forceUpdate")};function lt(){}lt.prototype=te.prototype;function Ae(d,j,R){this.props=d,this.context=j,this.refs=fe,this.updater=R||J}var ke=Ae.prototype=new lt;ke.constructor=Ae,$(ke,te.prototype),ke.isPureReactComponent=!0;var Ve=Array.isArray;function De(){}var ee={H:null,A:null,T:null,S:null},Se=Object.prototype.hasOwnProperty;function ot(d,j,R){var G=R.ref;return{$$typeof:f,type:d,key:j,ref:G!==void 0?G:null,props:R}}function Mt(d,j){return ot(d.type,j,d.props)}function de(d){return typeof d=="object"&&d!==null&&d.$$typeof===f}function Ze(d){var j={"=":"=0",":":"=2"};return"$"+d.replace(/[=:]/g,function(R){return j[R]})}var ct=/\/+/g;function Ke(d,j){return typeof d=="object"&&d!==null&&d.key!=null?Ze(""+d.key):j.toString(36)}function Le(d){switch(d.status){case"fulfilled":return d.value;case"rejected":throw d.reason;default:switch(typeof d.status=="string"?d.then(De,De):(d.status="pending",d.then(function(j){d.status==="pending"&&(d.status="fulfilled",d.value=j)},function(j){d.status==="pending"&&(d.status="rejected",d.reason=j)})),d.status){case"fulfilled":return d.value;case"rejected":throw d.reason}}throw d}function _(d,j,R,G,V){var I=typeof d;(I==="undefined"||I==="boolean")&&(d=null);var se=!1;if(d===null)se=!0;else switch(I){case"bigint":case"string":case"number":se=!0;break;case"object":switch(d.$$typeof){case f:case b:se=!0;break;case A:return se=d._init,_(se(d._payload),j,R,G,V)}}if(se)return V=V(d),se=G===""?"."+Ke(d,0):G,Ve(V)?(R="",se!=null&&(R=se.replace(ct,"$&/")+"/"),_(V,j,R,"",function(yt){return yt})):V!=null&&(de(V)&&(V=Mt(V,R+(V.key==null||d&&d.key===V.key?"":(""+V.key).replace(ct,"$&/")+"/")+se)),j.push(V)),1;se=0;var Qe=G===""?".":G+":";if(Ve(d))for(var Me=0;Me<d.length;Me++)G=d[Me],I=Qe+Ke(G,Me),se+=_(G,j,R,I,V);else if(Me=P(d),typeof Me=="function")for(d=Me.call(d),Me=0;!(G=d.next()).done;)G=G.value,I=Qe+Ke(G,Me++),se+=_(G,j,R,I,V);else if(I==="object"){if(typeof d.then=="function")return _(Le(d),j,R,G,V);throw j=String(d),Error("Objects are not valid as a React child (found: "+(j==="[object Object]"?"object with keys {"+Object.keys(d).join(", ")+"}":j)+"). If you meant to render a collection of children, use an array instead.")}return se}function C(d,j,R){if(d==null)return d;var G=[],V=0;return _(d,G,"","",function(I){return j.call(R,I,V++)}),G}function Z(d){if(d._status===-1){var j=d._result;j=j(),j.then(function(R){(d._status===0||d._status===-1)&&(d._status=1,d._result=R)},function(R){(d._status===0||d._status===-1)&&(d._status=2,d._result=R)}),d._staLine truncated
at`)?" (<anonymous>)":-1<l.stack.indexOf("@")?"@unknown:0:0":""}return`
`+Je+e+Ht}var k=!1;function ie(e,t){if(!e||k)return"";k=!0;var l=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var a={DetermineComponentFrameRoot:function(){try{if(t){var M=function(){throw Error()};if(Object.defineProperty(M.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(M,[])}catch(S){var v=S}Reflect.construct(e,[],M)}else{try{M.call()}catch(S){v=S}e.call(M.prototype)}}else{try{throw Error()}catch(S){v=S}(M=e())&&typeof M.catch=="function"&&M.catch(function(){})}}catch(S){if(S&&v&&typeof S.stack=="string")return[S.stack,v.stack]}return[null,null]}};a.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var n=Object.getOwnPropertyDescriptor(a.DetermineComponentFrameRoot,"name");n&&n.configurable&&Object.defineProperty(a.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var i=a.DetermineComponentFrameRoot(),u=i[0],s=i[1];if(u&&s){var o=u.split(`
`),p=s.split(`
`);for(n=a=0;a<o.length&&!o[a].includes("DetermineComponentFrameRoot");)a++;for(;n<p.length&&!p[n].includes("DetermineComponentFrameRoot");)n++;if(a===o.length||n===p.length)for(a=o.length-1,n=p.length-1;1<=a&&0<=n&&o[a]!==p[n];)n--;for(;1<=a&&0<=n;a--,n--)if(o[a]!==p[n]){if(a!==1||n!==1)do if(a--,n--,0>n||o[a]!==p[n]){var z=`
`+o[a].replace(" at new "," at ");return e.displayName&&z.includes("<anonymous>")&&(z=z.replace("<anonymous>",e.displayName)),z}while(1<=a&&0<=n);break}}}finally{k=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?zt(l):""}function D(e,t){switch(e.tag){case 26:case 27:case 5:return zt(e.type);case 16:return zt("Lazy");case 13:return e.child!==t&&t!==null?zt("Suspense Fallback"):zt("Suspense");case 19:return zt("SuspenseList");case 0:case 15:return ie(e.type,!1);case 11:return ie(e.type.render,!1);case 1:return ie(e.type,!0);case 31:return zt("Activity");default:return""}}function B(e){try{var t="",l=null;do t+=D(e,l),l=e,e=e.return;while(e);return t}catch(a){return`
Error generating stack: `+a.message+`
`+a.stack}}var W=Object.prototype.hasOwnProperty,ae=f.unstable_scheduleCallback,At=f.unstable_cancelCallback,Gt=f.unstable_shouldYield,dl=f.unstable_requestPaint,we=f.unstable_now,al=f.unstable_getCurrentPriorityLevel,$e=f.unstable_ImmediatePriority,za=f.unstable_UserBlockingPriority,Ta=f.unstable_NormalPriority,L=f.unstable_LowPriority,he=f.unstable_IdlePriority,Re=f.log,gt=f.unstable_setDisableYieldValue,st=null,Pe=null;function qt(e){if(typeof Re=="function"&&gt(e),Pe&&typeof Pe.setStrictMode=="function")try{Pe.setStrictMode(st,e)}catch{}}var rt=Math.clz32?Math.clz32:be,pt=Math.log,ve=Math.LN2;function be(e){return e>>>=0,e===0?32:31-(pt(e)/ve|0)|0}var Be=256,xe=262144,We=4194304;function _e(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function dt(e,t,l){var a=e.pendingLanes;if(a===0)return 0;var n=0,i=e.suspendedLanes,u=e.pingedLanes;e=e.warmLanes;var s=a&134217727;return s!==0?(a=s&~i,a!==0?n=_e(a):(u&=s,u!==0?n=_e(u):l||(l=s&~e,l!==0&&(n=_e(l))))):(s=a&~i,s!==0?n=_e(s):u!==0?n=_e(u):l||(l=a&~e,l!==0&&(n=_e(l)))),n===0?0:t!==0&&t!==n&&(t&i)===0&&(i=n&-n,l=t&-t,i>=l||i===32&&(l&4194048)!==0)?t:n}function vt(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function cl(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function ci(){var e=We;return We<<=1,(We&62914560)===0&&(We=4194304),e}function ju(e){for(var t=[],l=0;31>l;l++)t.push(e);return t}function mn(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function ym(e,t,l,a,n,i){var u=e.pendingLanes;e.pendingLanes=l,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=l,e.entangledLanes&=l,e.errorRecoveryDisabledLanes&=l,e.shellSuspendCounter=0;var s=e.entanglements,o=e.expirationTimes,p=e.hiddenUpdates;for(l=u&~l;0<l;){var z=31-rt(l),M=1<<z;s[z]=0,o[z]=-1;var v=p[z];if(v!==null)for(p[z]=null,z=0;z<v.length;z++){var S=v[z];S!==null&&(S.lane&=-536870913)}l&=~M}a!==0&&$s(e,a,0),i!==0&&n===0&&e.tag!==0&&(e.suspendedLanes|=i&~(u&~t))}function $s(e,t,l){e.pendingLanes|=t,e.suspendedLanes&=~t;var a=31-rt(t);e.entangledLanes|=t,e.entanglements[a]=e.entanglements[a]|1073741824|l&261930}function Ws(e,t){var l=e.entangledLanes|=t;for(e=e.entanglements;l;){var a=31-rt(l),n=1<<a;n&t|e[a]&t&&(e[a]|=t),l&=~n}}function Fs(e,t){var l=t&-t;return l=(l&42)!==0?1:zu(l),(l&(e.suspendedLanes|t))!==0?0:l}function zu(e){switch(e){case 2:e=1;break;case 8:e=4;break;case 32:e=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:e=128;break;case 268435456:e=134217728;break;default:e=0}return e}function Tu(e){return e&=-e,2<e?8<e?(e&134217727)!==0?32:268435456:8:2}function Is(){var e=C.p;return e!==0?e:(e=window.event,e===void 0?32:qd(e.type))}function Ps(e,t){var l=C.p;try{return C.p=e,t()}finally{C.p=l}}var Ol=Math.random().toString(36).slice(2),bt="__reactFiber$"+Ol,Dt="__reactProps$"+Ol,Ma="__reactContainer$"+Ol,Mu="__reactEvents$"+Ol,gm="__reactListeners$"+Ol,pm="__reactHandles$"+Ol,ef="__reactResources$"+Ol,hn="__reactMarker$"+Ol;function Nu(e){delete e[bt],delete e[Dt],delete e[Mu],delete e[gm],delete e[pm]}function Na(e){var t=e[bt];if(t)return t;for(var l=e.parentNode;l;){if(t=l[Ma]||l[bt]){if(l=t.alternate,t.child!==null||l!==null&&l.child!==null)for(e=jd(e);e!==null;){if(l=e[bt])return l;e=jd(e)}return t}e=l,l=e.parentNode}return null}function Ea(e){if(e=e[bt]||e[Ma]){var t=e.tag;if(t===5||t===6||t===13||t===31||t===26||t===27||t===3)return e}return null}function yn(e){var t=e.tag;if(t===5||t===26||t===27||t===6)return e.stateNode;throw Error(r(33))}function Aa(e){var t=e[ef];return t||(t=e[ef]={hoistableStyles:new Map,hoistableScripts:new Map}),t}function mt(e){e[hn]=!0}var tf=new Set,lf={};function ua(e,t){Da(e,t),Da(e+"Capture",t)}function Da(e,t){for(lf[e]=t,e=0;e<t.length;e++)tf.add(t[e])}var vm=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F Line truncated
`).replace($h,"")}function md(e,t){return t=dd(t),dd(e)===t}function Oe(e,t,l,a,n,i){switch(l){case"children":typeof a=="string"?t==="body"||t==="textarea"&&a===""||Oa(e,a):(typeof a=="number"||typeof a=="bigint")&&t!=="body"&&Oa(e,""+a);break;case"className":fi(e,"class",a);break;case"tabIndex":fi(e,"tabindex",a);break;case"dir":case"role":case"viewBox":case"width":case"height":fi(e,l,a);break;case"style":df(e,a,i);break;case"data":if(t!=="object"){fi(e,"data",a);break}case"src":case"href":if(a===""&&(t!=="a"||l!=="href")){e.removeAttribute(l);break}if(a==null||typeof a=="function"||typeof a=="symbol"||typeof a=="boolean"){e.removeAttribute(l);break}a=ri(""+a),e.setAttribute(l,a);break;case"action":case"formAction":if(typeof a=="function"){e.setAttribute(l,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof i=="function"&&(l==="formAction"?(t!=="input"&&Oe(e,t,"name",n.name,n,null),Oe(e,t,"formEncType",n.formEncType,n,null),Oe(e,t,"formMethod",n.formMethod,n,null),Oe(e,t,"formTarget",n.formTarget,n,null)):(Oe(e,t,"encType",n.encType,n,null),Oe(e,t,"method",n.method,n,null),Oe(e,t,"target",n.target,n,null)));if(a==null||typeof a=="symbol"||typeof a=="boolean"){e.removeAttribute(l);break}a=ri(""+a),e.setAttribute(l,a);break;case"onClick":a!=null&&(e.onclick=hl);break;case"onScroll":a!=null&&re("scroll",e);break;case"onScrollEnd":a!=null&&re("scrollend",e);break;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(r(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(r(60));e.innerHTML=l}}break;case"multiple":e.multiple=a&&typeof a!="function"&&typeof a!="symbol";break;case"muted":e.muted=a&&typeof a!="function"&&typeof a!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(a==null||typeof a=="function"||typeof a=="boolean"||typeof a=="symbol"){e.removeAttribute("xlink:href");break}l=ri(""+a),e.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",l);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":a!=null&&typeof a!="function"&&typeof a!="symbol"?e.setAttribute(l,""+a):e.removeAttribute(l);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":a&&typeof a!="function"&&typeof a!="symbol"?e.setAttribute(l,""):e.removeAttribute(l);break;case"capture":case"download":a===!0?e.setAttribute(l,""):a!==!1&&a!=null&&typeof a!="function"&&typeof a!="symbol"?e.setAttribute(l,a):e.removeAttribute(l);break;case"cols":case"rows":case"size":case"span":a!=null&&typeof a!="function"&&typeof a!="symbol"&&!isNaN(a)&&1<=a?e.setAttribute(l,a):e.removeAttribute(l);break;case"rowSpan":case"start":a==null||typeof a=="function"||typeof a=="symbol"||isNaN(a)?e.removeAttribute(l):e.setAttribute(l,a);break;case"popover":re("beforetoggle",e),re("toggle",e),si(e,"popover",a);break;case"xlinkActuate":ml(e,"http://www.w3.org/1999/xlink","xlink:actuate",a);break;case"xlinkArcrole":ml(e,"http://www.w3.org/1999/xlink","xlink:arcrole",a);break;case"xlinkRole":ml(e,"http://www.w3.org/1999/xlink","xlink:role",a);break;case"xlinkShow":ml(e,"http://www.w3.org/1999/xlink","xlink:show",a);break;case"xlinkTitle":ml(e,"http://www.w3.org/1999/xlink","xlink:title",a);break;case"xlinkType":ml(e,"http://www.w3.org/1999/xlink","xlink:type",a);break;case"xmlBase":ml(e,"http://www.w3.org/XML/1998/namespace","xml:base",a);break;case"xmlLang":ml(e,"http://www.w3.org/XML/1998/namespace","xml:lang",a);break;case"xmlSpace":ml(e,"http://www.w3.org/XML/1998/namespace","xml:space",a);break;case"is":si(e,"is",a);break;case"innerText":case"textContent":break;default:(!(2<l.length)||l[0]!=="o"&&l[0]!=="O"||l[1]!=="n"&&l[1]!=="N")&&(l=jm.get(l)||l,si(e,l,a))}}function ps(e,t,l,a,n,i){switch(l){case"style":df(e,a,i);break;case"dangerouslySetInnerHTML":if(a!=null){if(typeof a!="object"||!("__html"in a))throw Error(r(61));if(l=a.__html,l!=null){if(n.children!=null)throw Error(r(60));e.innerHTML=l}}break;case"children":typeof a=="string"?Oa(e,a):(typeof a=="number"||typeof a=="bigint")&&Oa(e,""+a);break;case"onScroll":a!=null&&re("scroll",e);break;case"onScrollEnd":a!=null&&re("scrollend",e);break;case"onClick":a!=null&&(e.onclick=hl);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"inne Line truncated
Seed: ${f.seed}${f.duration_sec?`
Time: ${f.duration_sec.toFixed(1)}s`:""}`,children:[c.jsx("img",{src:f.url,className:"gallery-thumb",alt:"Thumbnail",loading:"lazy",decoding:"async"}),N&&c.jsx("div",{className:"gallery-select-badge",children:c.jsx(Xs,{size:14})})]})});function lg({prompt:f,setPrompt:b,negativePrompt:N,setNegativePrompt:r,constraints:x,setConstraints:E,activeModel:q,generatedImages:H,setGeneratedImages:w,isGenerating:h,setIsGenerating:A,generationProgress:O,setGenerationProgress:Q,setActiveTab:P,showAlert:J=async({message:fe})=>window.alert(fe),showConfirm:$=async({message:fe})=>window.confirm(fe)}){const[fe,te]=U.useState(null),[lt,Ae]=U.useState(null),[ke,Ve]=U.useState(null),[De,ee]=U.useState(!1),[Se,ot]=U.useState(0),[Mt,de]=U.useState(0),[Ze,ct]=U.useState(0),[Ke,Le]=U.useState(""),[_,C]=U.useState(!1),[Z,me]=U.useState(!1),[ce,d]=U.useState(!1),[j,R]=U.useState(null),[G,V]=U.useState(null),[I,se]=U.useState(!1),[Qe,Me]=U.useState([]),[yt,Nt]=U.useState(null),Je=U.useRef(null),Ht=U.useRef(null),zt=U.useRef(!1),k=L=>{const he=L.target.files?.[0];if(L.target.value="",!he)return;if(!he.type.startsWith("image/")){V("Please choose an image file (PNG, JPG, or WEBP).");return}const Re=new FileReader;Re.onload=()=>Nt(Re.result),Re.onerror=()=>V("Could not read the selected image file."),Re.readAsDataURL(he)},ie=()=>Nt(null),D=async()=>{if(!f.trim())return;V(null),d(!0),R(null);let L=!1;try{const ve=await Cl();if(ve&&ve.settings){const be=ve.settings,Be=be.model?be.model.split(/[\\/]/).pop():null,xe=q?q.split(/[\\/]/).pop():null;(Be!==xe||parseInt(be.steps)!==parseInt(x.steps)||Math.abs(parseFloat(be.cfgScale)-parseFloat(x.cfgScale))>.05||be.sampler!==x.sampler||parseInt(be.threads)!==parseInt(x.threads)||!!be.useGpu!=(x.useGpu!==!1)||(be.backendType||(be.useGpu===!1?"cpu":"auto"))!==(x.backendType||(x.useGpu===!1?"cpu":"auto")))&&(L=!0)}else L=!0}catch{L=!0}if(L)try{console.log("Settings out of sync, restarting backend..."),await Vs(q,x);let ve=!1,be=null;for(let Be=0;Be<240;Be++){const xe=await Cl();if(xe.loading&&R({progress:xe.loading.progress||0,phase:xe.loading.phase||"Loading model...",speed:xe.loading.speed||"",current:xe.loading.current||0,total:xe.loading.total||0,backendMode:xe.loading.backendMode||xe.settings?.backendMode||"",device:xe.loading.device||xe.settings?.backendDevice||""}),xe.ready){ve=!0;break}if(xe.error){be=xe.error;break}if(!xe.running&&Be>3){be="The backend process terminated immediately on startup.";break}await new Promise(We=>setTimeout(We,500))}if(!ve)throw new Error(be||"Backend server failed to start after applying configuration changes.")}catch(ve){console.error("Failed to restart backend:",ve),V(ve.message||"Failed to sync settings with backend."),d(!1);return}d(!1),R(null),Je.current&&clearInterval(Je.current),A(!0),Q(0),ot(0),ct(0),Le(""),zt.current=!1,C(!1),se(!1),Ve(null),te(null);const he=(x.width>=1024?140:35)*.04,Re=x.width>=1024?140:35,gt=(x.backendType||(x.useGpu===!1?"cpu":"auto"))!=="cpu";let st=gt?he:Re,Pe=!1,qt=0,rt=0,pt=0;de(Math.round(x.steps*st)),Je.current=setInterval(async()=>{pt++,ot(pt);try{const ve=await eg();if(ve&&ve.active){const be=ve.decoding||!1;se(be);const Be=be?ve.steps||x.steps||20:ve.step||0,xe=ve.steps||x.steps||20,We=be?"":ve.speed||"";(be||ve.steps>0)&&(zt.current=!0,ct(Be)),Le(be?"decoding":We);let _e=0;be?_e=99:xe>0&&(_e=Math.round(Be/xe*100)),_e=Math.min(99,_e),be||ve.steps>0?Q(_e):Q(vt=>Math.max(vt,_e));let dt=0;if(be)dt=3;else{const vt=parseFloat(We);if(!isNaN(vt)&&vt>0){const cl=Math.max(0,xe-Be);We.includes("it/s")?dt=cl/vt:We.includes("s/it")&&(dt=cl*vt)}else{const cl=gt?he:Re;dt=Math.max(0,(xe-Be)*cl)}}if(gt&&!Pe){const vt=We.includes("it/s")&&numericSpeed<.2,cl=We.includes("s/it")&&numericSpeed>5;(vt||cl)&&(Pe=!0,C(!0))}de(Math.round(dt))}else Q(be=>Math.min(15,be+1)),de(be=>Math.max(1,be-1))}catch(ve){console.warn("Failed to get generation progress, running simulation fallback:",ve),gt&&!Pe&&pt>x.steps*he*1.3&&(Pe=!0,C(!0),st=Re,qt=pt,rt=Math.min(95,Math.round(pt/(x.steps*he)*100)));let be=Math.min(x.steps-1,Math.floor(pt/st)),Be=x.steps*st;if(pt>=Be){const _e=Math.ceil(pt/Math.max(.5,be));st=Math.max(st+15,_e+15),Be=x.steps*st,be=Math.min(x.steps-1,Math.floor(pt/st))}const xe=Math.max(1,Be-pt);de(Math.round(xe));let We=0;if(Pe){const _e=99-rt,dt=Be-qt;if(dt>0){const vt=Math.min(1,(pt-qt)/dt);if(vt<.85)We=Math.round(rt+vt*_e);else{const cl=(pt-(qt+dt*.85))/(st*2),ci=1-Math.exp(-cl);We=Math.min(99,Math.round(rt+.85*_e+.14*_e*ci))}}else We=99}else if(pt<Be*.85)We=Math.round(pt/Be*100);else{const _e=(pt-Be*.85)/(st*2),dt=1-Math.exp(-_e);We=Math.min(99,Math.round(85+14*dt))}zt.current||Q(_e=>Math.max(_e,We))}},1e3);try{Ht.current=new AbortController;const ve=await K0(f,N,x,q,yt,_e=>Q(dt=>Math.max(dt,_e)),Ht.current.signal);Je.current&&clearInterval(Je.current),Q(100),ct(x.steps),de(0),Le("");const be={prompt:f,negativePrompt:N,seed:ve.seed,steps:x.steps,cfgScale:x.cfgScale,width:x.width,height:x.height,sampler:x.sampler,model:q,mode:yt?"img2img":"txtLine truncated
${dl}
Update the UI status anyway?`,confirmLabel:"Update UI"})&&(b(D),r(!0)):await q({title:"Backend Not Running",message:`C++ backend server is not running on port ${At}.
To run locally, start:
${dl}
Proceed in Simulation Mode instead?`,confirmLabel:"Proceed"})&&(b(D),r(!1));return}fe(D),lt({progress:0,phase:"Starting backend...",speed:"",current:0,total:0,model:D,backendMode:"",backendBinary:"",device:""});try{const Gt=await Vs(D,x);console.log(Gt);let dl=!1,we=null;for(let al=0;al<240;al++){const $e=await Cl();if($e.loading&&lt({progress:$e.loading.progress||0,phase:$e.loading.phase||"Loading model...",speed:$e.loading.speed||"",current:$e.loading.current||0,total:$e.loading.total||0,model:$e.loading.model||D,backendMode:$e.loading.backendMode||$e.settings?.backendMode||"",backendBinary:$e.loading.backendBinary||$e.settings?.backendBinary||"",device:$e.loading.device||$e.settings?.backendDevice||""}),$e.ready){dl=!0;break}if($e.error){we=$e.error;break}if(!$e.running&&al>3){we="The backend process terminated immediately on startup.";break}await new Promise(za=>setTimeout(za,500))}if(dl)lt(al=>({...al||{},progress:100,phase:"Model ready",speed:"",current:al?.current||0,total:al?.total||0,model:D})),b(D),r(!0);else throw new Error(we||`Model server failed to respond on port ${At}.`)}catch(Gt){console.error("Failed to load model:",Gt),E({title:"Model Load Failed",message:Gt.message||String(Gt),danger:!0})}finally{setTimeout(()=>lt(null),800),fe(null)}},yt=async()=>{const D=Le;_(null),await Nt(),D&&await Me(D)},Nt=async()=>{if(de)return;Ze(!0),Ke({progress:10,phase:"Stopping backend process..."});const D=setInterval(async()=>{try{const B=await Cl();B.unloading?.active?Ke({progress:B.unloading.progress||50,phase:B.unloading.phase||"Unloading model..."}):Ke(W=>({progress:Math.min(95,W.progress+10),phase:W.phase||"Unloading model..."}))}catch{}},300);try{await Js(),Ke({progress:100,phase:"Model unloaded"}),b(null),_(null),r(!1)}catch(B){console.error(B),E({title:"Unload Failed",message:B.message||String(B),danger:!0})}finally{clearInterval(D),setTimeout(()=>{Ze(!1),Ke({progress:0,phase:""})},500)}},Je=async D=>{if(await q({title:"Delete Model?",message:`Delete "${D}" from your drive?`,confirmLabel:"Delete",danger:!0}))try{await W0(D),await R(),f===D&&await Nt()}catch(B){console.error("Failed to delete model:",B),E({title:"Delete Failed",message:B.message||String(B),danger:!0})}},Ht=async(D,B)=>{const W=new AbortController;Mt(W),ke(0),De({filename:B,speed:"0.0",eta:0,status:"Initiating file copy..."});try{await J0(D,ae=>{ke(Math.round(ae.progress)),De({filename:ae.filename,speed:ae.speed_mb_s.toFixed(1),eta:Math.round(ae.eta_secs),status:ae.status})},W.signal),await R()}catch(ae){console.error("Import failed:",ae),ae.name!=="AbortError"&&E({title:"Import Failed",message:ae.message||String(ae),danger:!0})}finally{ke(null),Mt(null)}},zt=()=>{ot&&ot.abort()},k=D=>{const B=D.target.files[0];B&&Ht(B,B.name),D.target.value=""},ie=!!h;return c.jsxs("div",{className:"workspace-area",children:[c.jsxs("div",{className:"workspace-title-section",children:[c.jsx("h2",{className:"workspace-title",children:"Model Manager"}),c.jsx("p",{className:"workspace-subtitle",children:"Manage local files and download recommended weights directly to your local models folder."})]}),f&&c.jsx("div",{className:"m3-card",style:{borderLeft:"4px solid var(--md-sys-color-primary)",background:"var(--md-sys-color-primary-container)",color:"var(--md-sys-color-on-primary-container)"},children:c.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[c.jsxs("div",{children:[c.jsxs("h4",{style:{fontWeight:700},children:["Active Model: ",f]}),c.jsx("p",{style:{fontSize:"0.85rem",marginTop:"2px",opacity:.9},children:"The local C++ stable-diffusion server is running. Real-time telemetry is displaying performance status in the top bar."}),(C.backendMode||C.backendDevice||C.backendBinary)&&c.jsxs("div",{style:{display:"flex",flexWrap:"wrap",gap:"8px",marginTop:"10px"},children:[C.backendMode&&c.jsxs("span",{className:"status-chip",style:{cursor:"default"},children:[c.jsx(xu,{size:14}),c.jsxs("span",{children:["Loaded on ",C.backendMode]})]}),C.backendDevice&&c.jsx("span",{className:"status-chip",style:{cursor:"default"},children:c.jsx("span",{children:C.backendDevice})}),C.backendBinary&&c.jsx("span",{className:"status-chip",style:{cursor:"default"},children:c.jsx("span",{children:C.backendBinary})})]})]}),c.jsxs("button",{className:"m3-btn m3-btn-error",onClick:Nt,disabled:de||$!==null,children:[de?c.jsx(aa,{className:"progress-spinner",size:16}):c.jsx(rn,{size:16}),c.jsx("span",{children:de?"Unloading":"Unload Server"})]})]})}),de&&c.jsxs("div",{className:"m3-card",style:{borderLeft:"4px solid var(--md-sys-color-error)",marginTop:"16px"},children:[c.jsx("h4",{style:{fontWeight:700,marginBottom:"8px"},children:"Unloading Model"}),c.jsxs("div",{className:"model-progress-section",style:{margin:"8px 0 0 0"},children:[c.jsxs("div",{className:"model-progress-label",children:[c.jsx("span",{children:ct.phase||"Unloading model..."}),c.jsxs("span",{children:[Math.round(ct.progress),"%"]})]}),c.jsx("div",{className:"model- Line truncated
Models and generated outputs are not included.`,confirmLabel:"Clean",danger:!0})){de(!0);try{await G0(Se.map(D=>D.id)),await I()}catch(D){E({title:"Cleanup Failed",message:D.message||String(D),danger:!0})}finally{de(!1)}}},[Se,I,E,x]);U.useEffect(()=>{async function k(){const ie=await Q0();Le(ie)}k()},[]),U.useEffect(()=>{J?localStorage.setItem("active-model",J):localStorage.removeItem("active-model")},[J]),U.useEffect(()=>{let k=null;async function ie(){const D=await Cl();if(!!D.ready){k&&clearInterval(k);const W=localStorage.getItem("active-model");if(W)$(W),te(!0);else{try{if(D&&D.settings&&D.settings.model){const ae=D.settings.model.split(/[\\/]/).pop();$(ae)}}catch{}te(!0)}}else if(!(typeof window<"u"&&window.__TAURI_INTERNALS__!==void 0)){const ae=localStorage.getItem("active-model");ae&&($(ae),te(!1))}}return ie(),k=setInterval(ie,2e3),()=>{k&&clearInterval(k)}},[]),U.useEffect(()=>{async function k(){try{const D=await L0();R(B=>B.cpu_usage===D.cpu_usage&&B.ram_used_gb===D.ram_used_gb&&B.ram_total_gb===D.ram_total_gb&&B.gpu_name===D.gpu_name&&B.vram_used_gb===D.vram_used_gb&&B.vram_total_gb===D.vram_total_gb?B:D)}catch{}}k();const ie=setInterval(k,1500);return()=>clearInterval(ie)},[]),U.useEffect(()=>{if(J){const k=J.toLowerCase();k.includes("flux")||k.includes("schnell")?V(ie=>({...ie,steps:4,cfgScale:1,width:1024,height:1024})):k.includes("lightning")||k.includes("turbo")?V(ie=>({...ie,steps:4,cfgScale:1.5,width:1024,height:1024})):k.includes("sd15")?V(ie=>({...ie,steps:25,cfgScale:7,width:512,height:512})):k.includes("sd35")?V(ie=>({...ie,steps:20,cfgScale:4.5,width:1024,height:1024})):V(ie=>({...ie,steps:20,cfgScale:7,width:512,height:512}))}},[J]);const Me=U.useMemo(()=>{switch(w){case"generator":return c.jsx(ag,{prompt:A,setPrompt:O,negativePrompt:Q,setNegativePrompt:P,constraints:G,setConstraints:V,activeModel:J,generatedImages:Ke,setGeneratedImages:Le,isGenerating:_,setIsGenerating:C,generationProgress:Z,setGenerationProgress:me,setActiveTab:h,showAlert:E,showConfirm:x});case"models":return c.jsx(ug,{activeModel:J,setActiveModel:$,serverRunning:fe,setServerRunning:te,constraints:G,showAlert:E,showConfirm:x});case"constraints":return c.jsx(fg,{constraints:G,setConstraints:V,activeModel:J,specs:ce,backendOptions:ke});default:return null}},[w,A,Q,G,J,Ke,_,Z,fe,ce,ke,E,x]),yt=U.useMemo(()=>c.jsx(E0,{activeTab:w,setActiveTab:h,specs:ce}),[w,ce]),Nt=U.useCallback(async()=>{if(!(!fe||lt)){Ae(!0);try{await Js(),te(!1),$(null)}catch(k){console.error("Failed to stop server:",k),E({title:"Stop Server Failed",message:k.message||String(k),danger:!0})}finally{Ae(!1)}}},[fe,lt,E]),Je=[...De?.stale?["Restart Local AI Image Generator so the local server loads the latest API."]:[],...De?.issues||[]],Ht=Se.reduce((k,ie)=>k+Number(ie.sizeBytes||0),0),zt=!!(De&&(Je.length>0||Se.length>0));return c.jsxs("div",{className:"app-container",children:[yt,c.jsxs("div",{className:"main-content",children:[c.jsx(D0,{telemetry:j,serverRunning:fe,activeModel:J,onStopServer:Nt,isStoppingServer:lt,theme:q,setTheme:H}),zt&&c.jsxs("div",{className:`m3-card readiness-card ${De?.stale||Je.length>0?"readiness-card-warning":""}`,children:[c.jsxs("div",{className:"readiness-header",children:[c.jsxs("div",{children:[c.jsx("h3",{className:"m3-card-title",style:{marginBottom:"4px"},children:De?.stale?"Restart Required":Je.length>0?"System Readiness":"Safe Cleanup Available"}),c.jsx("p",{className:"m3-card-subtitle",style:{margin:0},children:De?.stale?`Running server build: ${De.build||"unknown"}`:Je.length>0?"Local AI Image Generator found setup items that may need attention.":`${Se.length} temporary item${Se.length===1?"":"s"} can be cleaned (${ii(Ht)}).`})]}),c.jsxs("div",{className:"readiness-actions",children:[c.jsx("button",{className:"m3-btn m3-btn-outlined",onClick:I,disabled:Mt,children:Mt?"Checking":"Refresh"}),c.jsx("button",{className:"m3-btn m3-btn-tonal",onClick:se,children:Ze?"Copied":"Copy Diagnostics"}),Se.length>0&&c.jsxs("button",{className:"m3-btn m3-btn-error",onClick:Qe,disabled:Mt,children:["Clean ",ii(Ht)]})]})]}),Je.length>0&&c.jsx("div",{className:"readiness-issues",children:Je.slice(0,4).map(k=>c.jsx("span",{children:k},k))}),Se.length>0&&c.jsx("div",{className:"readiness-cleanup-list",children:Se.slice(0,3).map(k=>c.jsxs("span",{title:k.path,children:[k.name," · ",k.size," · ",k.reason]},k.id))})]}),Me]}),b&&c.jsx("div",{className:"app-dialog-backdrop",role:"presentation",children:c.jsxs("div",{className:"app-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"app-dialog-title",children:[c.jsx("h3",{id:"app-dialog-title",className:"app-dialog-title",children:b.title}),c.jsx("p",{className:"app-dialog-message",children:b.message}),c.jsxs("div",{className:"app-dialog-actions",children:[b.type==="confirm"&&c.jsx("button",{className:"m3-btn m3-btn-outlined",onClick:()=>r(!1),children:b.cancelLabel}),c.jsx("button",{className:`m3-btn ${b.danger?"m3-btn-error":"m3-btn-filled"}`,onClick:()=>r(!0),children Line truncated
-1
View File
@@ -1 +0,0 @@
:root{font-family:Outfit,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--md-shape-corner-small: 8px;--md-shape-corner-medium: 12px;--md-shape-corner-large: 16px;--md-shape-corner-extra-large: 22px;--md-shape-corner-full: 9999px;--md-transition-duration: .25s;--md-transition-easing: cubic-bezier(.4, 0, .2, 1);--md-sys-color-primary: #4f46e5;--md-sys-color-on-primary: #ffffff;--md-sys-color-primary-container: rgba(79, 70, 229, .06);--md-sys-color-on-primary-container: #4338ca;--md-sys-color-secondary: #0ea5e9;--md-sys-color-on-secondary: #ffffff;--md-sys-color-secondary-container: rgba(14, 165, 233, .06);--md-sys-color-on-secondary-container: #0369a1;--md-sys-color-tertiary: #d946ef;--md-sys-color-on-tertiary: #ffffff;--md-sys-color-background: #f8fafc;--md-sys-color-on-background: #0f172a;--md-sys-color-surface: rgba(255, 255, 255, .75);--md-sys-color-on-surface: #0f172a;--md-sys-color-surface-variant: rgba(241, 245, 249, .8);--md-sys-color-on-surface-variant: #475569;--md-sys-color-outline: #64748b;--md-sys-color-outline-variant: #94a3b8;--border-color: rgba(15, 23, 42, .08);--border-color-muted: rgba(15, 23, 42, .05);--md-sys-color-error: #ef4444;--md-sys-color-on-error: #ffffff;--md-sys-color-success: #10b981;--md-sys-color-on-success: #ffffff;--md-sys-color-card-border: rgba(15, 23, 42, .06);--glass-shadow: 0 8px 32px 0 rgba(15, 23, 42, .04);--glass-blur: blur(16px)}html[data-theme=dark]{--md-sys-color-primary: #6366f1;--md-sys-color-on-primary: #ffffff;--md-sys-color-primary-container: rgba(99, 102, 241, .12);--md-sys-color-on-primary-container: #c7d2fe;--md-sys-color-secondary: #38bdf8;--md-sys-color-on-secondary: #0f172a;--md-sys-color-secondary-container: rgba(56, 189, 248, .08);--md-sys-color-on-secondary-container: #bae6fd;--md-sys-color-tertiary: #f472b6;--md-sys-color-on-tertiary: #0f172a;--md-sys-color-background: #090a0f;--md-sys-color-on-background: #f4f4f5;--md-sys-color-surface: rgba(18, 20, 29, .65);--md-sys-color-on-surface: #f4f4f5;--md-sys-color-surface-variant: rgba(26, 29, 41, .55);--md-sys-color-on-surface-variant: #a1a1aa;--md-sys-color-outline: #a1a1aa;--md-sys-color-outline-variant: #71717a;--border-color: rgba(255, 255, 255, .09);--border-color-muted: rgba(255, 255, 255, .05);--md-sys-color-error: #f87171;--md-sys-color-on-error: #ffffff;--md-sys-color-success: #34d399;--md-sys-color-on-success: #090a0f;--md-sys-color-card-border: rgba(255, 255, 255, .04);--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, .3);--glass-blur: blur(16px)}*{box-sizing:border-box;margin:0;padding:0}body{background-color:var(--md-sys-color-background);background-image:radial-gradient(at 0% 0%,var(--md-sys-color-primary-container) 0px,transparent 50%),radial-gradient(at 100% 0%,var(--md-sys-color-secondary-container) 0px,transparent 40%);color:var(--md-sys-color-on-background);overflow:hidden;height:100vh;width:100vw;font-size:16px;transition:background-color var(--md-transition-duration) ease}.app-container{display:flex;flex-direction:row;height:100vh;width:100vw;overflow:hidden}.sidebar{width:280px;background-color:var(--md-sys-color-surface);backdrop-filter:var(--glass-blur);-webkit-backdrop-filter:var(--glass-blur);border-right:1px solid var(--border-color);display:flex;flex-direction:column;padding:28px 18px;justify-content:space-between;box-shadow:var(--glass-shadow);z-index:11;transition:all var(--md-transition-duration) var(--md-transition-easing)}.sidebar-logo{display:flex;align-items:center;gap:12px;padding:0 12px 28px}.sidebar-logo-icon{color:var(--md-sys-color-primary);width:26px;height:26px;filter:drop-shadow(0 2px 8px rgba(99,102,241,.3))}.sidebar-logo-text{font-size:1.15rem;font-weight:700;letter-spacing:-.02em;color:var(--md-sys-color-on-background)}.nav-list{display:flex;flex-direction:column;gap:6px;margin-bottom:auto}.nav-item{display:flex;align-items:center;gap:12px;padding:10px 16px;height:48px;border-radius:var(--md-shape-corner-medium);color:var(--md-sys-color-on-surface-variant);font-size:.9rem;font-weight:500;cursor:pointer;transition:all var(--md-transition-duration) var(--md-transition-easing);-webkit-user-select:none;user-select:none;text-decoration:none;position:relative}.nav-item:hover{background-color:var(--md-sys-color-surface-variant);color:var(--md-sys-color-primary);transform:translate(2px)}.nav-item.active{background-color:var(--md-sys-color-primary-container);color:var(--md-sys-color-primary);font-weight:600}.nav-item.active:before{content:"";position:absolute;left:0;top:14px;bottom:14px;width:4px;background-color:var(--md-sys-color-primary);border-radius:0 4px 4px 0}.sidebar-footer{padding:18px 12px 0;border-top:1px solid var(--border-color);font-size:.78rem;color:var(--md-sys-color-on-surface-variant);display:flex;flex-direction:column;gap:6px;opacity:.95}.main-content{flex:1;display:flex;flex-direction:column;background-coLine truncated
-18
View File
@@ -1,18 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<title>Local AI Image Generator</title>
<script type="module" crossorigin src="/assets/index-CG3WZqfX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DW6G7wc2.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
+25 -1
View File
@@ -97,6 +97,8 @@ function App() {
width: 1024,
height: 1024,
steps: 4, // Recommended default steps for Flux.1 Schnell/Lightning
npuSteps: 4,
standardSteps: 20,
cfgScale: 1.0, // Recommended default CFG for Flux.1 Schnell
sampler: "euler_a",
seed: -1,
@@ -277,10 +279,22 @@ function App() {
useEffect(() => {
if (activeModel) {
const name = activeModel.toLowerCase();
if (name.includes("flux") || name.includes("schnell")) {
if (name.includes("lcm-dreamshaper-v7-fp16")) {
setConstraints((prev) => ({
...prev,
steps: 4,
npuSteps: 4,
cfgScale: 1.0,
width: 512,
height: 512,
backendType: "openvino-npu",
useGpu: true,
}));
} else if (name.includes("flux") || name.includes("schnell")) {
setConstraints((prev) => ({
...prev,
steps: 4,
standardSteps: 4,
cfgScale: 1.0,
width: 1024,
height: 1024,
@@ -289,6 +303,7 @@ function App() {
setConstraints((prev) => ({
...prev,
steps: 4,
standardSteps: 4,
cfgScale: 1.5,
width: 1024,
height: 1024,
@@ -297,6 +312,7 @@ function App() {
setConstraints((prev) => ({
...prev,
steps: 25,
standardSteps: 25,
cfgScale: 7.0,
width: 512,
height: 512,
@@ -305,6 +321,7 @@ function App() {
setConstraints((prev) => ({
...prev,
steps: 20,
standardSteps: 20,
cfgScale: 4.5,
width: 1024,
height: 1024,
@@ -314,6 +331,7 @@ function App() {
setConstraints((prev) => ({
...prev,
steps: 20,
standardSteps: 20,
cfgScale: 7.0,
width: 512,
height: 512,
@@ -353,6 +371,7 @@ function App() {
serverRunning={serverRunning}
setServerRunning={setServerRunning}
constraints={constraints}
backendOptions={backendOptions}
showAlert={showAlert}
showConfirm={showConfirm}
/>
@@ -365,6 +384,11 @@ function App() {
activeModel={activeModel}
specs={specs}
backendOptions={backendOptions}
serverRunning={serverRunning}
setServerRunning={setServerRunning}
setActiveModel={setActiveModel}
showAlert={showAlert}
showConfirm={showConfirm}
/>
);
default:
+5 -2
View File
@@ -98,6 +98,8 @@ function Generator({
settings.sampler !== constraints.sampler ||
parseInt(settings.threads) !== parseInt(constraints.threads) ||
Boolean(settings.useGpu) !== (constraints.useGpu !== false) ||
parseInt(settings.width || 512) !== parseInt(constraints.width || 512) ||
parseInt(settings.height || 512) !== parseInt(constraints.height || 512) ||
(settings.backendType || (settings.useGpu === false ? "cpu" : "auto")) !== (constraints.backendType || (constraints.useGpu === false ? "cpu" : "auto"))) {
needsRestart = true;
}
@@ -115,7 +117,8 @@ function Generator({
let isReady = false;
let crashError = null;
for (let i = 0; i < 240; i++) { // Poll for up to 120 seconds (240 * 500ms)
const maxStartupPolls = constraints.backendType === "openvino-npu" ? 1200 : 240;
for (let i = 0; i < maxStartupPolls; i++) {
const status = await getBackendStatus();
if (status.loading) {
setRestartLoadProgress({
@@ -136,7 +139,7 @@ function Generator({
crashError = status.error;
break;
}
if (!status.running && i > 3) {
if (!status.running && !status.loading?.active && i > 3) {
crashError = "The backend process terminated immediately on startup.";
break;
}
+114 -15
View File
@@ -1,5 +1,6 @@
import React, { memo } from "react";
import React, { memo, useEffect } from "react";
import { Crop, Sliders, Cpu, Info } from "lucide-react";
import { stopServer } from "../services/api";
const ASPECT_RATIOS = [
{ id: "1:1", label: "1:1 Square", width: 512, height: 512, sdxl_width: 1024, sdxl_height: 1024, desc: "Social posts & avatars" },
@@ -17,26 +18,55 @@ const isSD15OrCustomModel = (modelName) => {
return true;
};
function ImageConstraints({ constraints, setConstraints, activeModel, specs, backendOptions }) {
function ImageConstraints({
constraints,
setConstraints,
activeModel,
specs,
backendOptions,
serverRunning,
setServerRunning,
setActiveModel,
showAlert = async ({ message }) => window.alert(message),
showConfirm = async ({ message }) => window.confirm(message),
}) {
const isSD15OrCustom = activeModel ? isSD15OrCustomModel(activeModel) : false;
const isOpenVinoNpu = constraints.backendType === "openvino-npu";
const forceStandardMode = isSD15OrCustom && !isOpenVinoNpu;
const availableBackends = backendOptions?.options?.length
? backendOptions.options
: [{ id: "cpu", label: "CPU", available: true }];
useEffect(() => {
if (isOpenVinoNpu && constraints.steps > 8) {
setConstraints((prev) => ({ ...prev, steps: 8, npuSteps: 8 }));
}
}, [constraints.steps, isOpenVinoNpu, setConstraints]);
const updateConstraint = (key, value) => {
setConstraints((prev) => ({
...prev,
[key]: value
[key]: value,
...(key === "steps"
? isOpenVinoNpu
? { npuSteps: value }
: { standardSteps: value }
: {}),
}));
};
const handleAspectRatioChange = (ratio, sizeType) => {
if (isOpenVinoNpu && ratio !== "1:1") return;
const isSDXL = sizeType === "sdxl" && !isSD15OrCustom;
const selected = ASPECT_RATIOS.find((r) => r.id === ratio);
if (selected) {
let w = isSDXL ? selected.sdxl_width : selected.width;
let h = isSDXL ? selected.sdxl_height : selected.height;
if (isSD15OrCustom) {
if (isOpenVinoNpu) {
const size = constraints.width >= 1024 ? 1024 : 512;
w = size;
h = size;
} else if (isSD15OrCustom) {
if (w > h) {
h = Math.round((h * 512) / w);
w = 512;
@@ -52,11 +82,54 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
}
};
const handleBackendChange = (backendType) => {
const handleBackendChange = async (backendType) => {
const currentBackend = constraints.backendType || "cpu";
if (backendType === currentBackend) return;
const switchesAccelerator =
(currentBackend === "openvino-npu" && backendType !== "openvino-npu") ||
(currentBackend !== "openvino-npu" && backendType === "openvino-npu");
if (serverRunning && switchesAccelerator) {
const leavingNpu = currentBackend === "openvino-npu";
const confirmed = await showConfirm({
title: leavingNpu ? "Unload NPU Model?" : "Unload Model?",
message: leavingNpu
? "The OpenVINO NPU model must be unloaded before switching to the standard backend."
: "The active model must be unloaded before switching to the OpenVINO NPU backend.",
confirmLabel: "Unload",
cancelLabel: "Cancel",
danger: true,
});
if (!confirmed) return;
try {
await stopServer();
setServerRunning(false);
setActiveModel(null);
} catch (err) {
await showAlert({
title: "Unload Failed",
message: err.message || String(err),
danger: true,
});
return;
}
}
setConstraints((prev) => ({
...prev,
backendType,
useGpu: backendType !== "cpu",
steps: backendType === "openvino-npu"
? Math.max(1, Math.min(8, prev.npuSteps || 4))
: Math.max(1, Math.min(60, prev.standardSteps || 20)),
...(backendType === "openvino-npu"
? {
width: prev.width >= 1024 ? 1024 : 512,
height: prev.width >= 1024 ? 1024 : 512,
}
: {}),
}));
};
@@ -79,7 +152,25 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
1. Picture Size & Shape
</h3>
{isSD15OrCustom && (
{isOpenVinoNpu ? (
<div style={{
background: "rgba(99, 102, 241, 0.1)",
border: "1px solid var(--md-sys-color-primary)",
color: "var(--md-sys-color-on-surface)",
padding: "12px",
borderRadius: "8px",
marginBottom: "16px",
fontSize: "0.85rem",
display: "flex",
gap: "8px",
alignItems: "flex-start"
}}>
<Info size={16} style={{ color: "var(--md-sys-color-primary)", flexShrink: 0, marginTop: "2px" }} />
<div>
<strong>OpenVINO NPU Resolution:</strong> The NPU generates at its stable 512x512 resolution. HD mode produces a 1024x1024 output using high-quality Lanczos upscaling without recompiling the model.
</div>
</div>
) : isSD15OrCustom && (
<div style={{
background: "rgba(251, 191, 36, 0.1)",
border: "1px solid rgb(251, 191, 36)",
@@ -104,7 +195,7 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
<div className="m3-text-field">
<label className="m3-text-field-label">AI Generation Engine Optimization</label>
<div className="m3-segmented-button">
{isSD15OrCustom ? (
{forceStandardMode ? (
<div
className="m3-segment-item disabled"
style={{
@@ -115,7 +206,7 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
}}
title="High Quality Mode is disabled for SD 1.x models."
>
High Quality Mode (1024px)
{isOpenVinoNpu ? "HD Upscale (1024px)" : "High Quality Mode (1024px)"}
</div>
) : (
<div
@@ -125,11 +216,11 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
updateConstraint("height", 1024);
}}
>
High Quality Mode (1024px)
{isOpenVinoNpu ? "HD Upscale (1024px)" : "High Quality Mode (1024px)"}
</div>
)}
<div
className={`m3-segment-item ${constraints.width < 1024 || isSD15OrCustom ? "active" : ""}`}
className={`m3-segment-item ${constraints.width < 1024 ? "active" : ""}`}
onClick={() => {
updateConstraint("width", 512);
updateConstraint("height", 512);
@@ -139,7 +230,9 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
</div>
</div>
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", marginTop: "4px" }}>
{isSD15OrCustom
{isOpenVinoNpu
? "*HD mode generates at 512x512 on the NPU, then upscales to 1024x1024. It improves output size, but does not add native diffusion detail."
: isSD15OrCustom
? "*Standard Speed Mode (512px) is forced for SD 1.5 / custom models to ensure stable local generations."
: "*Use High Quality (1024px) for Flux or SDXL models. Use Standard (512px) for SD 1.5."}
</span>
@@ -151,10 +244,12 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px" }}>
{ASPECT_RATIOS.map((ratio) => {
const isActive = Math.abs((constraints.width / constraints.height) - (ratio.width / ratio.height)) < 0.1;
const isUnsupportedOpenVinoShape = isOpenVinoNpu && ratio.id !== "1:1";
return (
<button
key={ratio.id}
className={`m3-btn m3-btn-outlined aspect-ratio-btn ${isActive ? "active" : ""}`}
disabled={isUnsupportedOpenVinoShape}
style={{
height: "86px",
display: "flex",
@@ -165,12 +260,14 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
borderRadius: "var(--md-shape-corner-medium)",
borderColor: "var(--md-sys-color-outline-variant)"
}}
onClick={() => handleAspectRatioChange(ratio.id, isSD15OrCustom ? "sd15" : (constraints.width >= 1024 ? "sdxl" : "sd15"))}
onClick={() => handleAspectRatioChange(ratio.id, forceStandardMode ? "sd15" : (constraints.width >= 1024 ? "sdxl" : "sd15"))}
>
<div className={`aspect-ratio-preview ratio-${ratio.id.replace(":", "-")}`}></div>
<span style={{ fontWeight: 600, fontSize: "0.85rem", marginTop: "2px" }}>{ratio.label}</span>
<span style={{ fontSize: "0.68rem", opacity: 0.7 }}>
{isSD15OrCustom
{isUnsupportedOpenVinoShape
? "Not yet supported on NPU"
: forceStandardMode
? `Max: ${ratio.id === "1:1" ? "512x512" : ratio.id === "4:3" ? "512x384" : ratio.id === "16:9" ? "512x288" : "288x512"}`
: ratio.desc}
</span>
@@ -202,10 +299,12 @@ function ImageConstraints({ constraints, setConstraints, activeModel, specs, bac
value={constraints.steps}
onChange={(e) => updateConstraint("steps", parseInt(e.target.value))}
min="1"
max="60"
max={isOpenVinoNpu ? "8" : "60"}
/>
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)", lineHeight: "1.3" }}>
The number of times the AI cleans up the image. More steps = sharper details, but takes longer.
{isOpenVinoNpu
? "LCM OpenVINO uses 1-8 fast inference steps. Progress is reported after each completed NPU step."
: "The number of times the AI cleans up the image. More steps = sharper details, but takes longer."}
</span>
</div>
+73 -10
View File
@@ -1,6 +1,6 @@
import React, { memo, useState, useEffect } from "react";
import { FolderOpen, DownloadCloud, RefreshCw, Database, Trash2, Square, HardDrive, Library, AlertTriangle } from "lucide-react";
import { listLocalModels, startServer, stopServer, importModelFile, deleteModel, downloadModel, cancelModelDownload, getDownloadProgress, getBackendStatus, pingServer, formatBytes, normalizeModel } from "../services/api";
import { listLocalModels, startServer, stopServer, importModelFile, deleteModel, downloadModel, downloadOpenVinoModel, cancelModelDownload, getDownloadProgress, getBackendStatus, pingServer, formatBytes, normalizeModel } from "../services/api";
const MODEL_LIBRARY = [
{
@@ -60,8 +60,28 @@ const MODEL_LIBRARY = [
},
];
function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRunning, constraints, showAlert = async ({ message }) => window.alert(message), showConfirm = async ({ message }) => window.confirm(message) }) {
const OPENVINO_MODEL_LIBRARY = [
{
group: "Intel NPU - OpenVINO Test",
items: [
{
id: "lcm-dreamshaper-v7-fp16",
name: "LCM DreamShaper v7 FP16",
filename: "lcm-dreamshaper-v7-fp16",
format: "OpenVINO",
backendType: "openvino-npu",
approxSize: "2.0 GB",
resolution: "512x512",
notes: "NPU test model for Windows and Linux. Runs the text encoder on CPU, UNet on Intel NPU, and VAE decoder on Intel GPU or CPU fallback.",
url: "https://huggingface.co/OpenVINO/LCM_Dreamshaper_v7-fp16-ov",
},
],
},
];
function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRunning, constraints, backendOptions, showAlert = async ({ message }) => window.alert(message), showConfirm = async ({ message }) => window.confirm(message) }) {
const [localModels, setLocalModels] = useState([]);
const [isLoadingModels, setIsLoadingModels] = useState(true);
const [downloadingModelId, setDownloadingModelId] = useState(null);
const [downloadProgress, setDownloadProgress] = useState(0);
const [downloadSpeed, setDownloadSpeed] = useState("");
@@ -78,6 +98,9 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
const modelNames = localModels.map((model) => normalizeModel(model).filename);
const isBusy = loadingModelId !== null || isUnloading;
const openvinoSupported = Boolean(backendOptions?.openvinoNpu?.supported);
const visibleModelLibrary = openvinoSupported ? [...MODEL_LIBRARY, ...OPENVINO_MODEL_LIBRARY] : MODEL_LIBRARY;
const getLocalModelInfo = (modelId) => localModels.map(normalizeModel).find((model) => model.filename === modelId);
useEffect(() => {
fetchModels();
@@ -149,11 +172,14 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
};
const fetchModels = async () => {
setIsLoadingModels(true);
try {
const list = await listLocalModels();
setLocalModels(list.map(normalizeModel).filter((model) => model.filename));
} catch (e) {
console.error(e);
} finally {
setIsLoadingModels(false);
}
};
@@ -217,7 +243,21 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
setDownloadUrl("");
};
const handleLibraryDownload = (model) => {
const handleLibraryDownload = async (model) => {
if (model.backendType === "openvino-npu") {
if (downloadingModelId) return;
try {
const res = await downloadOpenVinoModel(model.id);
if (res.ok) {
startProgressPolling(model.filename);
} else {
showAlert({ title: "Download Failed", message: res.error || "Unknown error", danger: true });
}
} catch (err) {
showAlert({ title: "Download Error", message: err.message, danger: true });
}
return;
}
downloadByUrl(model.url, model.filename);
};
@@ -246,7 +286,11 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
if (!isTauriDesktop && !isLocalServerMode) {
const isAlreadyRunning = await pingServer();
const cmd = `.\\backend\\win\\vulkan\\sd-vulkan.exe --listen-port ${backendPort} --model .\\models\\${modelId} --threads 8`;
const modelInfo = getLocalModelInfo(modelId);
const isOpenVinoModel = modelInfo?.backendType === "openvino-npu";
const cmd = isOpenVinoModel
? `Run the platform launcher and load "${modelId}" from Model Manager on a configured Intel NPU machine.`
: `.\\backend\\win\\vulkan\\sd-vulkan.exe --listen-port ${backendPort} --model .\\models\\${modelId} --threads 8`;
if (isAlreadyRunning) {
if (await showConfirm({ title: "Web Browser Mode", message: `To load "${modelId}" on the active GPU server, restart your terminal backend with:\n\n${cmd}\n\nUpdate the UI status anyway?`, confirmLabel: "Update UI" })) {
@@ -275,12 +319,26 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
device: "",
});
try {
const response = await startServer(modelId, constraints);
const modelInfo = getLocalModelInfo(modelId);
const loadConstraints = modelInfo?.backendType === "openvino-npu"
? {
...constraints,
backendType: "openvino-npu",
useGpu: true,
width: constraints.width >= 1024 ? 1024 : 512,
height: constraints.height >= 1024 ? 1024 : 512,
steps: Math.max(1, Math.min(8, constraints.steps || 4)),
cfgScale: constraints.cfgScale || 1,
}
: constraints;
const response = await startServer(modelId, loadConstraints);
console.log(response);
let isReady = false;
let crashError = null;
for (let i = 0; i < 240; i++) { // Poll for up to 120 seconds (240 * 500ms)
const isOpenVinoModel = modelInfo?.backendType === "openvino-npu";
const maxStartupPolls = isOpenVinoModel ? 1200 : 240;
for (let i = 0; i < maxStartupPolls; i++) {
const status = await getBackendStatus();
if (status.loading) {
setModelLoadProgress({
@@ -303,7 +361,7 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
crashError = status.error;
break;
}
if (!status.running && i > 3) {
if (!status.running && !status.loading?.active && i > 3) {
crashError = "The backend process terminated immediately on startup.";
break;
}
@@ -574,7 +632,12 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
Local Models ({localModels.length})
</h3>
{localModels.length === 0 ? (
{isLoadingModels ? (
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", gap: "10px", padding: "24px 0", color: "var(--md-sys-color-outline)" }}>
<RefreshCw className="progress-spinner" size={16} />
<span style={{ fontSize: "0.9rem" }}>Scanning models folder...</span>
</div>
) : localModels.length === 0 ? (
<p style={{ fontSize: "0.9rem", color: "var(--md-sys-color-outline)", textAlign: "center", padding: "16px 0" }}>
No models detected in app/models/. Download from the library below or import a file.
</p>
@@ -601,7 +664,7 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
<div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
<span style={{ fontWeight: 600, fontSize: "0.95rem" }}>{filename}</span>
<span style={{ fontSize: "0.75rem", opacity: 0.8 }}>
Local Weights File • {model.size || formatBytes(model.sizeBytes)}
{model.backendType === "openvino-npu" ? "OpenVINO NPU Model" : model.format || "Local Weights File"} • {model.size || formatBytes(model.sizeBytes)}
</span>
</div>
@@ -654,7 +717,7 @@ function ModelManager({ activeModel, setActiveModel, serverRunning, setServerRun
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
{MODEL_LIBRARY.map((section) => (
{visibleModelLibrary.map((section) => (
<div key={section.group} className="m3-card" style={{ margin: 0 }}>
<h4 style={{ fontWeight: 700, marginBottom: "12px" }}>{section.group}</h4>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: "12px" }}>
+91 -6
View File
@@ -48,8 +48,12 @@ export function normalizeModel(model) {
}
return {
filename: model?.filename || model?.name || "",
name: model?.name || model?.filename || "",
sizeBytes: Number(model?.sizeBytes || 0),
size: model?.size || (model?.sizeBytes ? formatBytes(model.sizeBytes) : "Unknown"),
format: model?.format || "Local Weights File",
backendType: model?.backendType || "",
resolution: model?.resolution || "",
};
}
@@ -254,6 +258,8 @@ export async function startServer(modelPath, constraints) {
threads: constraints.threads || 8,
use_gpu: constraints.useGpu !== false,
backend_type: constraints.backendType || (constraints.useGpu === false ? "cpu" : "auto"),
width: constraints.width || 512,
height: constraints.height || 512,
vae_tiling: constraints.vaeTiling !== false,
vae_on_cpu: constraints.vaeOnCpu === true,
flash_attn: constraints.useFlashAttn !== false,
@@ -331,14 +337,34 @@ export async function deleteGeneratedOutputs(outputs) {
// List model files from the models folder (via management API in web mode)
export async function listModelsFromDisk() {
try {
const r = await fetch("/api/models");
const data = await r.json();
return (data.models || []).map(normalizeModel);
const [modelRes, openvino] = await Promise.all([
fetch("/api/models"),
listOpenVinoModels().catch(() => ({ supported: false, models: [] })),
]);
const data = await modelRes.json();
const normalModels = (data.models || []).map(normalizeModel);
const openvinoModels = openvino.supported
? (openvino.models || []).filter((model) => model.installed).map((model) => normalizeModel({
filename: model.id,
name: model.name,
sizeBytes: model.sizeBytes,
size: model.size,
format: "OpenVINO",
backendType: "openvino-npu",
resolution: model.resolution,
}))
: [];
return [...normalModels, ...openvinoModels];
} catch (_) {
return [];
}
}
export async function listOpenVinoModels() {
const res = await fetch("/api/openvino-models");
return await readJsonResponse(res, "The local server returned invalid OpenVINO model data.");
}
// Generate image (T2I / I2I)
// Handles API calls to sd-server. If the server is unreachable or returns an error,
// we surface that error to the UI instead of silently returning a fake placeholder.
@@ -346,8 +372,6 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM
console.log("Initiating image generation:", { prompt, negativePrompt, constraints, activeModelName });
const startTime = Date.now();
const baseUrl = await getBackendBaseUrl();
// Prepare payload based on standard stable-diffusion.cpp REST endpoint schemas
const payload = {
prompt: prompt,
@@ -362,6 +386,45 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM
denoising_strength: constraints.denoisingStrength || 0.7,
};
if (constraints.backendType === "openvino-npu") {
if (inputImageBase64) {
throw new Error("OpenVINO NPU test mode currently supports text-to-image only.");
}
const response = await fetch("/api/openvino-generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
signal,
body: JSON.stringify({
prompt: payload.prompt,
negative_prompt: payload.negative_prompt,
steps: payload.steps,
cfg_scale: payload.cfg_scale,
seed: payload.seed,
width: payload.width,
height: payload.height,
}),
});
const data = await readJsonResponse(response, "The local server returned an invalid OpenVINO generation response.");
const imgB64 = data?.data?.[0]?.b64_json;
if (!imgB64) throw new Error("OpenVINO generation did not return an image.");
const normalizedB64 = String(imgB64).replace(/^data:[^;]+;base64,/, "");
const header = atob(normalizedB64.slice(0, 24));
const isPng = header.charCodeAt(0) === 0x89 && header.slice(1, 4) === "PNG";
const isJpeg = header.charCodeAt(0) === 0xff && header.charCodeAt(1) === 0xd8 && header.charCodeAt(2) === 0xff;
const isWebp = header.slice(0, 4) === "RIFF" && header.slice(8, 12) === "WEBP";
if (!isPng && !isJpeg && !isWebp) {
throw new Error("OpenVINO generation returned an invalid image payload instead of a real PNG/JPEG/WebP.");
}
const durationSec = Number(data.duration_sec) || parseFloat(((Date.now() - startTime) / 1000).toFixed(1));
return {
image: `data:image/png;base64,${normalizedB64}`,
seed: data.data?.[0]?.seed ?? payload.seed,
duration_sec: durationSec,
};
}
const baseUrl = await getBackendBaseUrl();
// txt2img uses /v1/images/generations; img2img uses /sdapi/v1/img2img.
const isImg2Img = !!payload.image;
@@ -417,9 +480,17 @@ export async function generateImage(prompt, negativePrompt, constraints, activeM
// Response: { data: [{ b64_json: "..." }] }
const imgB64 = data?.data?.[0]?.b64_json ?? data?.images?.[0];
if (imgB64) {
const normalizedB64 = String(imgB64).replace(/^data:[^;]+;base64,/, "");
const header = atob(normalizedB64.slice(0, 24));
const isPng = header.charCodeAt(0) === 0x89 && header.slice(1, 4) === "PNG";
const isJpeg = header.charCodeAt(0) === 0xff && header.charCodeAt(1) === 0xd8 && header.charCodeAt(2) === 0xff;
const isWebp = header.slice(0, 4) === "RIFF" && header.slice(8, 12) === "WEBP";
if (!isPng && !isJpeg && !isWebp) {
throw new Error("Generation returned an invalid image payload instead of a real PNG/JPEG/WebP.");
}
const durationSec = parseFloat(((Date.now() - startTime) / 1000).toFixed(1));
return {
image: `data:image/png;base64,${imgB64}`,
image: `data:image/png;base64,${normalizedB64}`,
seed: data.data?.[0]?.seed ?? payload.seed,
duration_sec: durationSec,
};
@@ -655,6 +726,20 @@ export async function downloadModel(url) {
}
}
export async function downloadOpenVinoModel(modelId) {
try {
const res = await fetch("/api/download-openvino-model", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model_id: modelId })
});
return await readJsonResponse(res, "The local server returned an invalid OpenVINO download response.");
} catch (e) {
console.error("Failed to start OpenVINO model download:", e);
return { ok: false, error: e.message };
}
}
export async function cancelModelDownload() {
try {
const res = await fetch("/api/cancel-download", { method: "POST" });
Executable
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env bash
#
# Local AI Image Generator - Linux Launcher
# Double-click or run: ./linux.sh
# Use --max-perf to enable ROCm backend downloads on Linux first setup.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="$SCRIPT_DIR/app"
PLATFORM="$(uname -s)"
if [[ "$PLATFORM" != "Linux" ]]; then
echo "[ERROR] This script is for Linux only. Please run ./mac.sh on macOS." >&2
exit 1
fi
NODE_DIR="$APP_DIR/tools/node-linux"
NODE_BIN="$NODE_DIR/bin/node"
BACKEND_PATH="$APP_DIR/backend/linux/vulkan/sd-vulkan"
CPU_BACKEND_PATH="$APP_DIR/backend/linux/cpu/sd-cpu"
PLATFORM_LABEL="Linux"
DIST_INDEX="$APP_DIR/dist/index.html"
SETUP_SCRIPT="$SCRIPT_DIR/scripts/setup.sh"
SERVE_SCRIPT="$SCRIPT_DIR/scripts/serve.cjs"
FRONTEND_PORT="${FRONTEND_PORT:-1420}"
SETUP_REASON=""
SETUP_MODE="Repair"
MAX_PERF_FLAG=""
SETUP_OPENVINO=0
# Parse args
for arg in "$@"; do
case "$arg" in
--max-perf)
MAX_PERF_FLAG="--max-perf"
;;
--setup-openvino)
SETUP_OPENVINO=1
;;
*)
echo "[ERROR] Unknown option: $arg" >&2
echo "Usage: ./linux.sh [--max-perf] [--setup-openvino]" >&2
exit 1
;;
esac
done
if [[ $SETUP_OPENVINO -eq 1 ]]; then
bash "$SCRIPT_DIR/scripts/setup-openvino-npu.sh"
fi
# ── Symlink node_modules to avoid OS conflicts ──────────────────────────────
FRONTEND_NODE_MODULES="$APP_DIR/frontend/node_modules"
LINUX_NODE_MODULES="$APP_DIR/frontend/node_modules_linux"
if [[ -d "$FRONTEND_NODE_MODULES" && ! -L "$FRONTEND_NODE_MODULES" ]]; then
echo " >> Migrating existing node_modules to node_modules_linux..."
mv "$FRONTEND_NODE_MODULES" "$LINUX_NODE_MODULES"
fi
rm -f "$FRONTEND_NODE_MODULES"
mkdir -p "$LINUX_NODE_MODULES"
ln -sf "node_modules_linux" "$FRONTEND_NODE_MODULES"
# ── First-time setup check ─────────────────────────────────────────────────
if [[ ! -d "$NODE_DIR" ]]; then
SETUP_MODE="First-Time Setup"
fi
if [[ ! -x "$NODE_BIN" ]]; then
SETUP_REASON="Portable Node.js for Linux is missing."
fi
if [[ ! -f "$DIST_INDEX" ]]; then
SETUP_REASON="Frontend build is missing."
fi
# At minimum we need CPU or Vulkan backend on Linux, and both CLI and server binaries must be executable
CPU_SERVER_PATH="$APP_DIR/backend/linux/cpu/sd-server-cpu"
VULKAN_SERVER_PATH="$APP_DIR/backend/linux/vulkan/sd-server-vulkan"
if [[ ! -x "$CPU_BACKEND_PATH" || ! -x "$CPU_SERVER_PATH" ]] && [[ ! -x "$BACKEND_PATH" || ! -x "$VULKAN_SERVER_PATH" ]]; then
SETUP_REASON="Linux backend binaries are missing or not executable."
fi
if [[ -n "$SETUP_REASON" ]]; then
echo ""
echo " ============================================================"
echo " LOCAL AI IMAGE GENERATOR | $PLATFORM_LABEL $SETUP_MODE"
echo " ============================================================"
echo ""
if [[ "$SETUP_MODE" == "First-Time Setup" ]]; then
echo " This looks like your first run on Linux. Setting up automatically..."
else
echo " Local AI Image Generator needs a quick repair before launch."
fi
echo " Reason: $SETUP_REASON"
echo " Models are not downloaded during setup. Download or import them in the app."
echo ""
read -rp " Press Enter to continue, or Ctrl+C to cancel."
# Clear any existing frontend server process
if command -v fuser >/dev/null 2>&1; then
fuser -k "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true
fi
if ! bash "$SETUP_SCRIPT" $MAX_PERF_FLAG; then
echo ""
echo " [ERROR] Setup failed. Please check the output above."
read -rp " Press Enter to close..."
exit 1
fi
fi
# ── Launch ─────────────────────────────────────────────────────────────────
clear 2>/dev/null || true
echo ""
echo " ============================================================"
echo " LOCAL AI IMAGE GENERATOR | Launching..."
echo " ============================================================"
echo ""
# Clear frontend port
if command -v fuser >/dev/null 2>&1; then
fuser -k "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true
fi
# Start the server
echo " Starting Local AI Image Generator..."
export PATH="$NODE_DIR/bin:$PATH"
export FRONTEND_PORT="$FRONTEND_PORT"
# Run server in background and capture PID
"$NODE_BIN" "$SERVE_SCRIPT" &
SERVER_PID=$!
# Wait for server to be ready
sleep 2
# Open browser
if command -v xdg-open >/dev/null 2>&1; then
echo " Opening browser at http://localhost:${FRONTEND_PORT}"
xdg-open "http://localhost:${FRONTEND_PORT}" >/dev/null 2>&1 &
else
echo " Open your browser to: http://localhost:${FRONTEND_PORT}"
fi
echo ""
echo " ============================================================"
echo " Running!"
echo " Web UI: http://localhost:${FRONTEND_PORT}"
echo " GPU API: Auto-selected by the app (starts at 8080)"
echo ""
echo " Press Ctrl+C in this window to stop all services."
echo " ============================================================"
echo ""
# Cleanup on exit
cleanup() {
echo ""
echo " Shutting down..."
if kill -0 "$SERVER_PID" >/dev/null 2>&1; then
kill -TERM "$SERVER_PID" >/dev/null 2>&1 || true
sleep 1
kill -KILL "$SERVER_PID" >/dev/null 2>&1 || true
fi
echo " Done. Goodbye!"
exit 0
}
trap cleanup SIGINT SIGTERM
# Keep script alive
wait "$SERVER_PID" || true
cleanup
+28 -34
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env bash
#
# Local AI Image Generator - Linux/macOS Launcher
# Double-click or run: ./start.sh
# Use --max-perf to enable ROCm backend downloads on Linux first setup.
# Local AI Image Generator - macOS Launcher
# Double-click or run: ./mac.sh
#
set -euo pipefail
@@ -10,17 +9,17 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="$SCRIPT_DIR/app"
PLATFORM="$(uname -s)"
if [[ "$PLATFORM" == "Darwin" ]]; then
NODE_DIR="$APP_DIR/tools/node-mac"
BACKEND_PATH="$APP_DIR/backend/mac/sd"
PLATFORM_LABEL="macOS"
else
NODE_DIR="$APP_DIR/tools/node-linux"
BACKEND_PATH="$APP_DIR/backend/linux/vulkan/sd-vulkan"
CPU_BACKEND_PATH="$APP_DIR/backend/linux/cpu/sd-cpu"
PLATFORM_LABEL="Linux"
if [[ "$PLATFORM" != "Darwin" ]]; then
echo "[ERROR] This script is for macOS only. Please run ./linux.sh on Linux." >&2
exit 1
fi
NODE_DIR="$APP_DIR/tools/node-mac"
NODE_BIN="$NODE_DIR/bin/node"
BACKEND_PATH="$APP_DIR/backend/mac/sd"
PLATFORM_LABEL="macOS"
DIST_INDEX="$APP_DIR/dist/index.html"
SETUP_SCRIPT="$SCRIPT_DIR/scripts/setup.sh"
SERVE_SCRIPT="$SCRIPT_DIR/scripts/serve.cjs"
@@ -28,37 +27,35 @@ SERVE_SCRIPT="$SCRIPT_DIR/scripts/serve.cjs"
FRONTEND_PORT="${FRONTEND_PORT:-1420}"
SETUP_REASON=""
SETUP_MODE="Repair"
MAX_PERF_FLAG=""
# Parse args
if [[ "${1:-}" == "--max-perf" ]]; then
MAX_PERF_FLAG="--max-perf"
# ── Symlink node_modules to avoid OS conflicts ──────────────────────────────
FRONTEND_NODE_MODULES="$APP_DIR/frontend/node_modules"
MAC_NODE_MODULES="$APP_DIR/frontend/node_modules_mac"
if [[ -d "$FRONTEND_NODE_MODULES" && ! -L "$FRONTEND_NODE_MODULES" ]]; then
echo " >> Migrating existing node_modules to node_modules_mac..."
mv "$FRONTEND_NODE_MODULES" "$MAC_NODE_MODULES"
fi
rm -f "$FRONTEND_NODE_MODULES"
mkdir -p "$MAC_NODE_MODULES"
ln -sf "node_modules_mac" "$FRONTEND_NODE_MODULES"
# ── First-time setup check ─────────────────────────────────────────────────
if [[ ! -d "$NODE_DIR" ]]; then
SETUP_MODE="First-Time Setup"
fi
if [[ ! -x "$NODE_BIN" ]]; then
SETUP_REASON="Portable Node.js is missing."
SETUP_REASON="Portable Node.js for macOS is missing."
fi
if [[ ! -f "$DIST_INDEX" ]]; then
SETUP_REASON="Frontend build is missing."
fi
if [[ "$PLATFORM" == "Darwin" ]]; then
if [[ ! -x "$BACKEND_PATH" ]]; then
SETUP_REASON="No macOS Metal backend binary is installed."
fi
else
# At minimum we need CPU or Vulkan backend on Linux, and both CLI and server binaries must be executable
CPU_SERVER_PATH="$APP_DIR/backend/linux/cpu/sd-server-cpu"
VULKAN_SERVER_PATH="$APP_DIR/backend/linux/vulkan/sd-server-vulkan"
if [[ ! -x "$CPU_BACKEND_PATH" || ! -x "$CPU_SERVER_PATH" ]] && [[ ! -x "$BACKEND_PATH" || ! -x "$VULKAN_SERVER_PATH" ]]; then
SETUP_REASON="Linux backend binaries are missing or not executable."
fi
if [[ ! -x "$BACKEND_PATH" ]]; then
SETUP_REASON="No macOS Metal backend binary is installed."
fi
if [[ -n "$SETUP_REASON" ]]; then
@@ -68,7 +65,7 @@ if [[ -n "$SETUP_REASON" ]]; then
echo " ============================================================"
echo ""
if [[ "$SETUP_MODE" == "First-Time Setup" ]]; then
echo " This looks like your first run. Setting up automatically..."
echo " This looks like your first run on macOS. Setting up automatically..."
else
echo " Local AI Image Generator needs a quick repair before launch."
fi
@@ -82,7 +79,7 @@ if [[ -n "$SETUP_REASON" ]]; then
fuser -k "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true
fi
if ! bash "$SETUP_SCRIPT" $MAX_PERF_FLAG; then
if ! bash "$SETUP_SCRIPT"; then
echo ""
echo " [ERROR] Setup failed. Please check the output above."
read -rp " Press Enter to close..."
@@ -116,12 +113,9 @@ SERVER_PID=$!
sleep 2
# Open browser
if [[ "$PLATFORM" == "Darwin" ]] && command -v open >/dev/null 2>&1; then
if command -v open >/dev/null 2>&1; then
echo " Opening browser at http://localhost:${FRONTEND_PORT}"
open "http://localhost:${FRONTEND_PORT}" >/dev/null 2>&1 &
elif command -v xdg-open >/dev/null 2>&1; then
echo " Opening browser at http://localhost:${FRONTEND_PORT}"
xdg-open "http://localhost:${FRONTEND_PORT}" >/dev/null 2>&1 &
else
echo " Open your browser to: http://localhost:${FRONTEND_PORT}"
fi
+187
View File
@@ -0,0 +1,187 @@
import argparse
import base64
import json
import sys
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from io import BytesIO
from pathlib import Path
import openvino as ov
import openvino_genai as genai
from PIL import Image
class WorkerState:
def __init__(self, model_dir: Path, width: int, height: int, cache_dir: Path):
self.model_dir = model_dir
self.width = width
self.height = height
self.cache_dir = cache_dir
self.ready = False
self.error = None
self.pipeline = None
self.devices = {}
self.started_at = time.time()
def load(self) -> None:
try:
available_devices = ov.Core().available_devices
if "NPU" not in available_devices:
raise RuntimeError(
f"Intel NPU is not available to OpenVINO. Devices: {', '.join(available_devices)}"
)
vae_device = "GPU" if "GPU" in available_devices else "CPU"
self.devices = {
"text_encoder": "CPU",
"unet": "NPU",
"vae_decoder": vae_device,
}
print(f"[openvino-npu] Loading model: {self.model_dir}", flush=True)
print(
"[openvino-npu] Devices: "
f"text_encoder=CPU, unet=NPU, vae_decoder={vae_device}",
flush=True,
)
self.cache_dir.mkdir(parents=True, exist_ok=True)
pipe = genai.Text2ImagePipeline(str(self.model_dir))
pipe.reshape(1, self.height, self.width, 1.0)
pipe.compile(
"CPU",
"NPU",
vae_device,
config={"CACHE_DIR": str(self.cache_dir)},
)
self.pipeline = pipe
self.ready = True
print("[openvino-npu] READY", flush=True)
except Exception as exc:
self.error = str(exc)
print(f"[openvino-npu] ERROR: {self.error}", file=sys.stderr, flush=True)
raise
def make_handler(state: WorkerState):
class Handler(BaseHTTPRequestHandler):
def _json(self, code: int, payload: dict) -> None:
body = json.dumps(payload).encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_OPTIONS(self) -> None:
self.send_response(204)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
self.end_headers()
def do_GET(self) -> None:
if self.path == "/health":
self._json(200, {
"ok": state.ready and not state.error,
"ready": state.ready,
"error": state.error,
"model": str(state.model_dir),
"devices": state.devices,
"uptime_sec": round(time.time() - state.started_at, 1),
})
return
self._json(404, {"ok": False, "error": "Unknown endpoint"})
def do_POST(self) -> None:
if self.path != "/generate":
self._json(404, {"ok": False, "error": "Unknown endpoint"})
return
if not state.ready or state.pipeline is None:
self._json(503, {"ok": False, "error": state.error or "OpenVINO NPU worker is not ready"})
return
length = int(self.headers.get("Content-Length", "0") or "0")
try:
request = json.loads(self.rfile.read(length).decode("utf-8") or "{}")
prompt = str(request.get("prompt") or "").strip()
if not prompt:
raise ValueError("Prompt is required")
steps = max(1, min(8, int(request.get("steps") or 4)))
guidance_scale = float(request.get("guidance_scale") or request.get("cfg_scale") or 1.0)
width = int(request.get("width") or state.width)
height = int(request.get("height") or state.height)
if (width, height) not in ((512, 512), (1024, 1024)):
raise ValueError("OpenVINO NPU supports 512x512 generation or 1024x1024 HD upscale.")
started = time.time()
last_step_at = started
def progress_callback(step: int, num_steps: int, _latent) -> bool:
nonlocal last_step_at
now = time.time()
step_duration = max(0.001, now - last_step_at)
last_step_at = now
print(
f"[openvino-npu] PROGRESS {step + 1}/{num_steps} {1.0 / step_duration:.2f} it/s",
flush=True,
)
return False
generate_args = {
"width": state.width,
"height": state.height,
"num_inference_steps": steps,
"guidance_scale": guidance_scale,
"rng_seed": int(request.get("seed")) if request.get("seed") not in (None, -1) else int(time.time_ns() % (2**32)),
"callback": progress_callback,
}
negative_prompt = str(request.get("negative_prompt") or "").strip()
if negative_prompt and guidance_scale > 1.0:
generate_args["negative_prompt"] = negative_prompt
image_tensor = state.pipeline.generate(prompt, **generate_args)
print("[openvino-npu] DECODING", flush=True)
image = Image.fromarray(image_tensor.data[0])
upscaled = width != state.width or height != state.height
if upscaled:
image = image.resize((width, height), Image.Resampling.LANCZOS)
buf = BytesIO()
image.save(buf, format="PNG")
encoded = base64.b64encode(buf.getvalue()).decode("ascii")
self._json(200, {
"ok": True,
"data": [{
"b64_json": encoded,
"seed": request.get("seed"),
}],
"generated_size": f"{state.width}x{state.height}",
"output_size": f"{width}x{height}",
"upscaled": upscaled,
"duration_sec": round(time.time() - started, 2),
})
except Exception as exc:
self._json(500, {"ok": False, "error": str(exc)})
def log_message(self, fmt: str, *args) -> None:
print(f"[openvino-npu-http] {fmt % args}", flush=True)
return Handler
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--model-dir", required=True, type=Path)
parser.add_argument("--port", required=True, type=int)
parser.add_argument("--width", type=int, default=512)
parser.add_argument("--height", type=int, default=512)
parser.add_argument("--cache-dir", type=Path, default=Path("app/tools/openvino-cache"))
args = parser.parse_args()
state = WorkerState(args.model_dir, args.width, args.height, args.cache_dir)
state.load()
server = ThreadingHTTPServer(("127.0.0.1", args.port), make_handler(state))
print(f"[openvino-npu] Listening on http://127.0.0.1:{args.port}", flush=True)
server.serve_forever()
if __name__ == "__main__":
main()
+13
View File
@@ -45,6 +45,19 @@ if (Test-Path $nodeModulesDir) {
Remove-Item $nodeModulesDir -Recurse -Force -ErrorAction SilentlyContinue
}
$nodeModulesMac = Join-Path $appDir "frontend\node_modules_mac"
if (Test-Path $nodeModulesMac) {
Write-Host " >> Removing frontend node_modules_mac..." -ForegroundColor Cyan
Remove-Item $nodeModulesMac -Recurse -Force -ErrorAction SilentlyContinue
}
$nodeModulesLinux = Join-Path $appDir "frontend\node_modules_linux"
if (Test-Path $nodeModulesLinux) {
Write-Host " >> Removing frontend node_modules_linux..." -ForegroundColor Cyan
Remove-Item $nodeModulesLinux -Recurse -Force -ErrorAction SilentlyContinue
}
# Delete package-lock.json in frontend
$lockFile = Join-Path $appDir "frontend\package-lock.json"
if (Test-Path $lockFile) {
+11 -1
View File
@@ -40,11 +40,21 @@ if [[ -d "$APP_DIR/models" ]]; then
fi
# Delete node_modules in frontend
if [[ -d "$APP_DIR/frontend/node_modules" ]]; then
if [[ -L "$APP_DIR/frontend/node_modules" || -d "$APP_DIR/frontend/node_modules" ]]; then
echo " >> Removing frontend node_modules..."
rm -rf "$APP_DIR/frontend/node_modules"
fi
if [[ -d "$APP_DIR/frontend/node_modules_mac" ]]; then
echo " >> Removing frontend node_modules_mac..."
rm -rf "$APP_DIR/frontend/node_modules_mac"
fi
if [[ -d "$APP_DIR/frontend/node_modules_linux" ]]; then
echo " >> Removing frontend node_modules_linux..."
rm -rf "$APP_DIR/frontend/node_modules_linux"
fi
# Delete package-lock.json in frontend
if [[ -f "$APP_DIR/frontend/package-lock.json" ]]; then
echo " >> Removing frontend package-lock.json..."
+547 -48
View File
@@ -34,6 +34,7 @@ const BACKEND_PATHS = {
linuxCuda: path.join(ROOT, "app", "backend", "linux", "cuda", "sd-server-cuda"),
};
let BACKEND_PATH = "";
const backendSupportsFlags = {};
if (osPlatform === "win32") {
let hasNvidia = false;
try {
@@ -67,15 +68,38 @@ const MODELS = path.join(ROOT, "app", "models");
if (!fs.existsSync(MODELS)) {
fs.mkdirSync(MODELS, { recursive: true });
}
const OPENVINO_MODELS = path.join(ROOT, "app", "openvino-models");
if (!fs.existsSync(OPENVINO_MODELS)) {
fs.mkdirSync(OPENVINO_MODELS, { recursive: true });
}
const OUTPUTS = path.join(ROOT, "app", "outputs");
if (!fs.existsSync(OUTPUTS)) {
fs.mkdirSync(OUTPUTS, { recursive: true });
}
const OPENVINO_NPU_MODELS = [
{
id: "lcm-dreamshaper-v7-fp16",
name: "LCM DreamShaper v7 FP16",
repo: "OpenVINO/LCM_Dreamshaper_v7-fp16-ov",
folder: "LCM_Dreamshaper_v7-fp16-ov",
approxSize: "2.0 GB",
resolution: "512x512",
notes: "OpenVINO LCM image model. Uses CPU text encoder, NPU UNet, and GPU or CPU VAE decoder.",
},
];
// ── Backend process state ─────────────────────────────────────────────────────
let backendProc = null;
let backendReady = false;
let backendError = null;
let openvinoProc = null;
let openvinoReady = false;
let openvinoError = null;
let openvinoPort = null;
let openvinoModel = null;
let openvinoWidth = null;
let openvinoHeight = null;
let backendLoadState = {
active: false,
phase: "",
@@ -104,6 +128,8 @@ let currentSettings = {
vaeTiling: true,
vaeOnCpu: false,
flashAttn: true,
width: 512,
height: 512,
};
let lastCpuSample = null;
@@ -396,6 +422,110 @@ function getPathSize(targetPath) {
}
}
function getOpenVinoPythonCandidates() {
const candidates = [];
if (process.env.OPENVINO_PYTHON) candidates.push(process.env.OPENVINO_PYTHON);
if (osPlatform === "win32") {
candidates.push(path.join(ROOT, "app", "tools", "openvino-venv-win", "Scripts", "python.exe"));
candidates.push(path.join(ROOT, "app", "tools", "openvino-venv", "Scripts", "python.exe")); // legacy fallback
candidates.push("C:\\tmp\\npu-test-venv\\Scripts\\python.exe");
candidates.push("python");
} else {
candidates.push(path.join(ROOT, "app", "tools", "openvino-venv-linux", "bin", "python"));
candidates.push(path.join(ROOT, "app", "tools", "openvino-venv", "bin", "python")); // legacy fallback
candidates.push("python3");
candidates.push("python");
}
return [...new Set(candidates)];
}
function getOpenVinoPython() {
for (const candidate of getOpenVinoPythonCandidates()) {
try {
const result = spawnSync(candidate, [
"-c",
"import openvino, openvino_genai, PIL; print(openvino.__version__)",
], { encoding: "utf8", timeout: 10000, stdio: ["ignore", "pipe", "pipe"] });
if (result.status === 0) return candidate;
} catch (_) {}
}
return null;
}
let cachedOpenVinoNpuInfo = null;
function getOpenVinoNpuInfo() {
if (cachedOpenVinoNpuInfo) return cachedOpenVinoNpuInfo;
if (osPlatform !== "win32" && osPlatform !== "linux") {
cachedOpenVinoNpuInfo = { supported: false, reason: "OpenVINO NPU backend is supported on Windows and Linux Intel Core Ultra systems." };
return cachedOpenVinoNpuInfo;
}
const python = getOpenVinoPython();
if (!python) {
const setupScript = osPlatform === "win32"
? "scripts/setup-openvino-npu.ps1"
: "bash scripts/setup-openvino-npu.sh";
cachedOpenVinoNpuInfo = {
supported: false,
reason: `OpenVINO GenAI runtime is not installed. Run ${setupScript} first.`,
};
return cachedOpenVinoNpuInfo;
}
try {
const script = [
"import json, openvino as ov",
"core=ov.Core()",
"devices=core.available_devices",
"info={'devices':devices,'npu':None}",
"if 'NPU' in devices:",
" info['npu']={'name':core.get_property('NPU','FULL_DEVICE_NAME'),'capabilities':core.get_property('NPU','OPTIMIZATION_CAPABILITIES')}",
"print(json.dumps(info))",
].join("\n");
const result = spawnSync(python, ["-c", script], { encoding: "utf8", timeout: 15000, stdio: ["ignore", "pipe", "pipe"] });
if (result.status !== 0) {
cachedOpenVinoNpuInfo = { supported: false, python, reason: result.stderr.trim() || "OpenVINO NPU probe failed." };
return cachedOpenVinoNpuInfo;
}
const info = JSON.parse(result.stdout.trim());
if (!info.devices.includes("NPU")) {
cachedOpenVinoNpuInfo = { supported: false, python, reason: `OpenVINO is installed, but NPU is not available. Devices: ${info.devices.join(", ")}` };
return cachedOpenVinoNpuInfo;
}
cachedOpenVinoNpuInfo = { supported: true, platform: osPlatform, python, devices: info.devices, npu: info.npu };
return cachedOpenVinoNpuInfo;
} catch (err) {
cachedOpenVinoNpuInfo = { supported: false, python, reason: err.message || String(err) };
return cachedOpenVinoNpuInfo;
}
}
function getOpenVinoModelInfo() {
return OPENVINO_NPU_MODELS.map((model) => {
const modelPath = path.join(OPENVINO_MODELS, model.folder);
const requiredFiles = [
path.join(modelPath, "model_index.json"),
path.join(modelPath, "text_encoder", "openvino_model.xml"),
path.join(modelPath, "unet", "openvino_model.xml"),
path.join(modelPath, "vae_decoder", "openvino_model.xml"),
];
const installed = requiredFiles.every((file) => fs.existsSync(file));
return {
...model,
filename: model.id,
path: modelPath,
installed,
sizeBytes: getPathSize(modelPath),
size: formatBytes(getPathSize(modelPath)),
format: "OpenVINO",
};
});
}
function findOpenVinoModel(modelId) {
return getOpenVinoModelInfo().find((model) => model.id === modelId || model.folder === modelId || model.name === modelId);
}
function pathInside(childPath, parentPath) {
const relative = path.relative(path.resolve(parentPath), path.resolve(childPath));
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
@@ -504,7 +634,7 @@ async function getHealth() {
const ports = {
frontend: { ...(await checkPort(PORT_FRONTEND)), expectedInUse: true },
backend: { ...(await checkPort(PORT_BACKEND)), expectedInUse: backendProc !== null },
backend: { ...(await checkPort(PORT_BACKEND)), expectedInUse: backendProc !== null || openvinoProc !== null },
};
ports.frontend.ok = !ports.frontend.available;
ports.backend.preferred = PREFERRED_BACKEND_PORT;
@@ -525,8 +655,8 @@ async function getHealth() {
checks,
ports,
backend: {
ready: backendReady,
running: backendProc !== null,
ready: backendReady || openvinoReady,
running: backendProc !== null || openvinoProc !== null,
error: backendError,
settings: currentSettings,
options: getBackendOptions(),
@@ -622,6 +752,32 @@ function hasAmdGpu() {
return info.includes("amd") || info.includes("advanced micro devices") || info.includes("radeon");
}
// Detect whether we are running inside WSL2 (Windows Subsystem for Linux).
// WSL2's GPU paravirtualization (GPU-PV) only exposes a D3D12/DirectX interface
// to Linux. Vulkan hardware passthrough is only supported for NVIDIA (via CUDA).
// Intel Arc and AMD GPUs in WSL2 fall back to llvmpipe (CPU software rendering).
function isRunningInWSL() {
if (osPlatform !== "linux") return false;
try {
const procVersion = fs.readFileSync("/proc/version", "utf8").toLowerCase();
return procVersion.includes("microsoft") || procVersion.includes("wsl");
} catch (_) {
return false;
}
}
function getVulkanUnavailableReason() {
if (osPlatform === "linux" && isRunningInWSL()) {
const gpuName = getGpuInfo().name;
const lowerGpu = gpuName.toLowerCase();
if (lowerGpu.includes("intel") || lowerGpu.includes("arc")) {
return `Vulkan GPU is not available in WSL2 for Intel ${gpuName}. WSL2's GPU paravirtualization only exposes a DirectX interface — Intel Arc Vulkan requires running natively on Windows.`;
}
return "Vulkan GPU is not available in WSL2. WSL2's GPU paravirtualization does not support hardware Vulkan for this GPU. Run natively on Windows or Linux for GPU acceleration.";
}
return "Installed, but this binary did not register a Vulkan backend on this machine.";
}
function getBackendOptions() {
if (cachedBackendOptions) return cachedBackendOptions;
@@ -643,6 +799,9 @@ function getBackendOptions() {
const metalInstalled = osPlatform === "darwin" && fs.existsSync(BACKEND_PATHS.mac);
const metalAvailable = metalInstalled;
const coremlAvailable = osPlatform === "darwin" && fs.existsSync("/Users/orailnoor/workspace/coreml_conversion/venv/bin/python");
const openvinoNpu = getOpenVinoNpuInfo();
const openvinoModels = getOpenVinoModelInfo();
const openvinoNpuAvailable = openvinoNpu.supported && openvinoModels.some((model) => model.installed);
const options = [{ id: "cpu", label: "CPU", available: true }];
if (metalAvailable) options.push({ id: "metal", label: "Metal GPU", available: true });
@@ -650,10 +809,11 @@ function getBackendOptions() {
if (vulkanAvailable) options.push({ id: "vulkan", label: "Vulkan GPU", available: true });
if (rocmAvailable) options.push({ id: "rocm", label: "ROCm GPU (AMD)", available: true });
if (cudaAvailable) options.push({ id: "cuda", label: "CUDA GPU", available: true });
if (openvinoNpuAvailable) options.push({ id: "openvino-npu", label: "NPU (OpenVINO)", available: true });
const unavailable = [];
if (vulkanInstalled && !vulkanAvailable) {
unavailable.push({ id: "vulkan", label: "Vulkan GPU", reason: "Installed, but this binary did not register a Vulkan backend on this machine." });
unavailable.push({ id: "vulkan", label: "Vulkan GPU", reason: getVulkanUnavailableReason() });
}
if (cudaInstalled && !cudaAvailable) {
unavailable.push({ id: "cuda", label: "CUDA GPU", reason: "Installed, but CUDA backend validation failed." });
@@ -664,6 +824,11 @@ function getBackendOptions() {
if (metalInstalled && !metalAvailable) {
unavailable.push({ id: "metal", label: "Metal GPU", reason: "Installed, but Metal backend validation failed." });
}
if (openvinoNpu.supported && !openvinoNpuAvailable) {
unavailable.push({ id: "openvino-npu", label: "NPU (OpenVINO)", reason: "Runtime is ready, but no OpenVINO NPU model is downloaded." });
} else if (!openvinoNpu.supported && (osPlatform === "win32" || osPlatform === "linux")) {
unavailable.push({ id: "openvino-npu", label: "NPU (OpenVINO)", reason: openvinoNpu.reason });
}
let defaultBackend = "cpu";
if (coremlAvailable) {
@@ -691,6 +856,9 @@ function getBackendOptions() {
vulkanAvailable,
rocmAvailable,
metalAvailable,
openvinoNpuAvailable,
openvinoNpu,
openvinoModels,
defaultBackendType: defaultBackend,
};
return cachedBackendOptions;
@@ -722,17 +890,19 @@ function backendAccepts(binaryPath, backendName) {
const existing = spawnEnv.DYLD_LIBRARY_PATH || "";
spawnEnv.DYLD_LIBRARY_PATH = dir + (existing ? ":" + existing : "");
}
let result = spawnSync(binaryPath, probeArgs, { encoding: "utf8", timeout: 5000, env: spawnEnv });
let result = spawnSync(binaryPath, probeArgs, { env: spawnEnv, encoding: "utf8", timeout: 5000 });
let output = `${result.stdout || ""}\n${result.stderr || ""}`;
let supportsFlags = true;
// Some binaries do not support --backend. If we see "unknown argument",
// retry without backend flags so we can still verify the binary launches.
if (output.includes("unknown argument") && output.includes("--backend")) {
supportsFlags = false;
const fallbackArgs = [
"--model", path.join(MODELS, "__backend_probe_missing__.safetensors"),
"--listen-port", "18082",
];
result = spawnSync(binaryPath, fallbackArgs, { encoding: "utf8", timeout: 5000, env: spawnEnv });
result = spawnSync(binaryPath, fallbackArgs, { env: spawnEnv, encoding: "utf8", timeout: 5000 });
output = `${result.stdout || ""}\n${result.stderr || ""}`;
}
@@ -745,7 +915,11 @@ function backendAccepts(binaryPath, backendName) {
return false;
}
// A healthy binary prints project-specific log lines when it tries to load the model.
return lower.includes("stable-diffusion.cpp") || lower.includes("loading model");
const isOk = lower.includes("stable-diffusion.cpp") || lower.includes("loading model");
if (isOk) {
backendSupportsFlags[binaryPath] = supportsFlags;
}
return isOk;
} catch (_) {
return false;
}
@@ -844,6 +1018,208 @@ function pingBackendReady() {
});
}
function requestJson(url, payload = null, timeoutMs = 120000) {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const data = payload ? Buffer.from(JSON.stringify(payload), "utf8") : null;
const req = http.request({
hostname: parsed.hostname,
port: parsed.port,
path: parsed.pathname + parsed.search,
method: data ? "POST" : "GET",
headers: data ? {
"Content-Type": "application/json",
"Content-Length": data.length,
} : {},
timeout: timeoutMs,
}, (res) => {
let body = "";
res.setEncoding("utf8");
res.on("data", (chunk) => { body += chunk; });
res.on("end", () => {
try {
const jsonBody = JSON.parse(body || "{}");
if (res.statusCode < 200 || res.statusCode >= 300 || jsonBody.ok === false) {
reject(new Error(jsonBody.error || `HTTP ${res.statusCode}`));
return;
}
resolve(jsonBody);
} catch (err) {
reject(err);
}
});
});
req.on("error", reject);
req.on("timeout", () => req.destroy(new Error("Request timed out")));
if (data) req.write(data);
req.end();
});
}
async function killOpenVinoWorker() {
if (!openvinoProc) {
openvinoReady = false;
return;
}
console.log(" [openvino-npu] Stopping worker...");
try { openvinoProc.kill("SIGTERM"); } catch (_) {}
await new Promise((resolve) => setTimeout(resolve, 800));
try { openvinoProc.kill("SIGKILL"); } catch (_) {}
openvinoProc = null;
openvinoReady = false;
}
function getOpenVinoResolution(settings = {}) {
const width = Number(settings.width) || 512;
const height = Number(settings.height) || 512;
const supported = (width === 512 && height === 512) || (width === 1024 && height === 1024);
if (!supported) {
throw new Error("OpenVINO NPU supports 512x512 generation or 1024x1024 HD upscale.");
}
return { width, height };
}
async function startOpenVinoWorker(settings = {}) {
const npuInfo = getOpenVinoNpuInfo();
if (!npuInfo.supported) throw new Error(npuInfo.reason || "OpenVINO NPU is not available.");
const model = findOpenVinoModel(settings.model) || getOpenVinoModelInfo().find((item) => item.installed);
if (!model || !model.installed) {
throw new Error("OpenVINO NPU model is not downloaded. Download an OpenVINO NPU model from Model Manager first.");
}
const { width, height } = getOpenVinoResolution(settings);
if (openvinoProc &&
openvinoReady &&
openvinoModel === model.id) {
currentSettings = { ...currentSettings, ...settings, model: model.id, width, height };
return;
}
await killBackend();
await killOpenVinoWorker();
openvinoError = null;
openvinoReady = false;
openvinoModel = model.id;
openvinoWidth = 512;
openvinoHeight = 512;
openvinoPort = await findAvailableBackendPort();
PORT_BACKEND = openvinoPort;
currentSettings = { ...currentSettings, ...settings };
currentSettings.backendType = "openvino-npu";
currentSettings.useGpu = true;
currentSettings.backendMode = "NPU (OpenVINO)";
currentSettings.backendBinary = "openvino_npu_worker.py";
currentSettings.backendDevice = npuInfo.npu?.name || "Intel AI Boost";
currentSettings.model = model.id;
currentSettings.width = width;
currentSettings.height = height;
backendError = null;
backendLoadState = {
active: true,
phase: "Compiling OpenVINO NPU pipeline (512x512)...",
progress: 5,
current: 0,
total: 0,
speed: "",
model: model.name,
backendMode: "NPU (OpenVINO)",
backendBinary: "openvino_npu_worker.py",
device: currentSettings.backendDevice,
};
const workerPath = path.join(ROOT, "scripts", "openvino_npu_worker.py");
const cacheDir = path.join(ROOT, "app", "tools", "openvino-cache", "512x512");
console.log(` [openvino-npu] Starting 512x512 worker on port ${openvinoPort}`);
openvinoProc = spawn(npuInfo.python, [
workerPath,
"--model-dir", model.path,
"--port", String(openvinoPort),
"--width", "512",
"--height", "512",
"--cache-dir", cacheDir,
], { stdio: "pipe" });
let openvinoStdoutBuffer = "";
openvinoProc.stdout.on("data", (data) => {
const text = data.toString();
process.stdout.write(" " + text);
openvinoStdoutBuffer += text;
const lines = openvinoStdoutBuffer.split(/\r?\n/);
openvinoStdoutBuffer = lines.pop() || "";
for (const line of lines) {
if (line.includes("READY")) {
openvinoReady = true;
backendReady = true;
backendLoadState = { ...backendLoadState, active: false, phase: "OpenVINO NPU model ready", progress: 100 };
}
const progressMatch = line.match(/PROGRESS\s+(\d+)\/(\d+)\s+(.+)$/);
if (progressMatch) {
generationState.active = true;
generationState.step = Number(progressMatch[1]);
generationState.steps = Number(progressMatch[2]);
generationState.speed = progressMatch[3].trim();
generationState.decoding = false;
} else if (line.includes("DECODING") && generationState.active) {
generationState.step = generationState.steps;
generationState.decoding = true;
generationState.speed = "";
}
}
});
openvinoProc.stderr.on("data", (data) => {
const text = data.toString();
process.stderr.write(" " + text);
const cleanText = stripAnsi(text).trim();
if (cleanText && (cleanText.includes("ERROR") || cleanText.includes("Traceback") || cleanText.includes("Exception"))) {
openvinoError = cleanText;
backendError = openvinoError;
}
});
openvinoProc.on("exit", (code) => {
console.log(" [openvino-npu] worker exited with code", code);
openvinoProc = null;
openvinoReady = false;
backendReady = false;
if (code !== 0 && code !== null && !backendError) backendError = `OpenVINO NPU worker exited with code ${code}`;
backendLoadState.active = false;
});
const startedAt = Date.now();
while (Date.now() - startedAt < 360000) {
if (openvinoReady) return;
if (backendError) throw new Error(backendError);
if (!openvinoProc) throw new Error("OpenVINO NPU worker exited during startup.");
const elapsed = Math.round((Date.now() - startedAt) / 1000);
backendLoadState.progress = Math.min(95, 5 + Math.round(elapsed / 2));
await new Promise((resolve) => setTimeout(resolve, 1000));
}
await killOpenVinoWorker();
throw new Error("OpenVINO NPU 512x512 compile timed out after 6 minutes.");
}
async function generateWithOpenVino(body) {
if (!openvinoReady || !openvinoPort) throw new Error("OpenVINO NPU worker is not ready. Load an OpenVINO NPU model first.");
const steps = Math.max(1, Math.min(8, Number(body.steps) || 4));
generationState = { active: true, step: 0, steps, speed: "", decoding: false };
try {
const result = await requestJson(`http://127.0.0.1:${openvinoPort}/generate`, {
prompt: body.prompt,
negative_prompt: body.negative_prompt || "",
width: Number(body.width) || currentSettings.width || 512,
height: Number(body.height) || currentSettings.height || 512,
steps,
cfg_scale: Number(body.cfg_scale) || 1.0,
seed: body.seed,
}, 300000);
resetGenerationState();
return result;
} catch (err) {
resetGenerationState();
throw err;
}
}
function startBackendReadyPoll() {
let attempts = 0;
const interval = setInterval(async () => {
@@ -888,6 +1264,11 @@ function killBackend() {
}
async function startBackend(settings = {}) {
if (settings.backendType === "openvino-npu") {
await startOpenVinoWorker(settings);
return;
}
await killOpenVinoWorker();
backendError = null;
currentSettings = { ...currentSettings, ...settings };
if (!currentSettings.model) currentSettings.model = getDefaultModel();
@@ -945,6 +1326,8 @@ async function startBackend(settings = {}) {
let args = [];
const requestedBackend = resolveBackendType(currentSettings.useGpu, currentSettings.backendType, currentSettings.model);
const supportsFlags = backendSupportsFlags[backendPath] !== false;
if (requestedBackend === "apple-npu") {
args = [
path.join(ROOT, "app", "backend", "mac", "coreml_server.py"),
@@ -964,39 +1347,30 @@ async function startBackend(settings = {}) {
];
if (requestedBackend === "cpu") {
args.push(
"--backend", "cpu",
"--params-backend", "cpu",
"--rng", "cpu",
"--sampler-rng", "cpu",
);
if (supportsFlags) {
args.push("--backend", "cpu", "--params-backend", "cpu");
}
args.push("--rng", "cpu", "--sampler-rng", "cpu");
} else if (requestedBackend === "vulkan") {
args.push(
"--backend", "vulkan0",
"--params-backend", "vulkan0",
"--rng", "cpu",
"--sampler-rng", "cpu",
);
if (supportsFlags) {
args.push("--backend", "vulkan0", "--params-backend", "vulkan0");
}
args.push("--rng", "cpu", "--sampler-rng", "cpu");
} else if (requestedBackend === "cuda") {
// CUDA is only supported on Windows. Linux has no reliable prebuilt CUDA binary.
args.push(
"--backend", "cuda0",
"--params-backend", "cuda0",
"--rng", "cuda",
"--sampler-rng", "cuda",
);
if (supportsFlags) {
args.push("--backend", "cuda0", "--params-backend", "cuda0");
}
args.push("--rng", "cuda", "--sampler-rng", "cuda");
} else if (requestedBackend === "rocm") {
args.push(
"--backend", "rocm0",
"--params-backend", "rocm0",
"--rng", "cpu",
"--sampler-rng", "cpu",
);
if (supportsFlags) {
args.push("--backend", "rocm0", "--params-backend", "rocm0");
}
args.push("--rng", "cpu", "--sampler-rng", "cpu");
} else if (requestedBackend === "metal") {
args.push(
"--rng", "cpu",
"--sampler-rng", "cpu",
);
if (supportsFlags) {
args.push("--backend", "metal0", "--params-backend", "metal0");
}
args.push("--rng", "cpu", "--sampler-rng", "cpu");
}
}
@@ -1193,6 +1567,54 @@ let downloadState = {
};
let activeDownload = null;
function startOpenVinoModelDownload(modelId) {
if (downloadState.active) return;
const npuInfo = getOpenVinoNpuInfo();
if (!npuInfo.supported) throw new Error(npuInfo.reason || "OpenVINO NPU runtime is not available.");
const model = OPENVINO_NPU_MODELS.find((item) => item.id === modelId);
if (!model) throw new Error("Unknown OpenVINO model.");
const destDir = path.join(OPENVINO_MODELS, model.folder);
fs.mkdirSync(destDir, { recursive: true });
downloadState = {
active: true,
filename: model.name,
progress: -1,
speed: "Downloading snapshot",
eta: 0,
totalBytes: 0,
downloadedBytes: 0,
error: null,
};
const script = [
"from huggingface_hub import snapshot_download",
`snapshot_download(repo_id=${JSON.stringify(model.repo)}, local_dir=${JSON.stringify(destDir)}, ignore_patterns=['safety_checker/*','feature_extractor/*','*.safetensors'])`,
"print('DONE')",
].join("\n");
console.log(` [openvino-download] Downloading ${model.repo} -> ${destDir}`);
const proc = spawn(npuInfo.python, ["-c", script], { stdio: "pipe" });
activeDownload = { process: proc, destPath: destDir, openvino: true };
proc.stdout.on("data", (data) => process.stdout.write(" [openvino-download] " + data.toString()));
proc.stderr.on("data", (data) => process.stderr.write(" [openvino-download] " + data.toString()));
proc.on("exit", (code) => {
activeDownload = null;
if (String(downloadState.error || "").toLowerCase().includes("cancelled")) {
return;
}
downloadState.active = false;
if (code === 0) {
downloadState.progress = 100;
downloadState.downloadedBytes = getPathSize(destDir);
downloadState.totalBytes = downloadState.downloadedBytes;
downloadState.speed = "Complete";
cachedBackendOptions = null;
} else {
downloadState.error = `OpenVINO model download failed with code ${code}`;
}
});
}
// ── Generation State (Real-time progress parser) ─────────────────────────────
let generationState = {
active: false,
@@ -1428,9 +1850,14 @@ function cancelModelDownload() {
}
const filename = downloadState.filename;
if (activeDownload) {
try { activeDownload.request.destroy(new Error("Download cancelled by user")); } catch (_) {}
try { activeDownload.fileStream.destroy(); } catch (_) {}
try { fs.unlinkSync(activeDownload.tempPath || activeDownload.destPath); } catch (_) {}
if (activeDownload.process) {
try { activeDownload.process.kill("SIGTERM"); } catch (_) {}
setTimeout(() => { try { activeDownload?.process?.kill("SIGKILL"); } catch (_) {} }, 1000);
} else {
try { activeDownload.request.destroy(new Error("Download cancelled by user")); } catch (_) {}
try { activeDownload.fileStream.destroy(); } catch (_) {}
try { fs.unlinkSync(activeDownload.tempPath || activeDownload.destPath); } catch (_) {}
}
} else if (filename) {
try { fs.unlinkSync(`${path.join(MODELS, filename)}.part`); } catch (_) {}
}
@@ -1600,9 +2027,9 @@ function safeOutputName(value) {
}
function saveGeneratedOutput(imageDataUrl, metadata = {}) {
const match = String(imageDataUrl || "").match(/^data:(image\/(?:png|jpeg|jpg|webp|svg\+xml));base64,(.+)$/);
const match = String(imageDataUrl || "").match(/^data:(image\/(?:png|jpeg|jpg|webp));base64,(.+)$/);
if (!match) {
throw new Error("Expected a base64 image data URL");
throw new Error("Expected a real base64 PNG, JPEG, or WebP image data URL");
}
const mime = match[1];
@@ -1611,9 +2038,26 @@ function saveGeneratedOutput(imageDataUrl, metadata = {}) {
"image/jpeg": ".jpg",
"image/jpg": ".jpg",
"image/webp": ".webp",
"image/svg+xml": ".svg",
};
const ext = extByMime[mime] || ".png";
const imageBuffer = Buffer.from(match[2], "base64");
const isPng = imageBuffer.length > 8 &&
imageBuffer[0] === 0x89 &&
imageBuffer[1] === 0x50 &&
imageBuffer[2] === 0x4e &&
imageBuffer[3] === 0x47;
const isJpeg = imageBuffer.length > 3 &&
imageBuffer[0] === 0xff &&
imageBuffer[1] === 0xd8 &&
imageBuffer[2] === 0xff;
const isWebp = imageBuffer.length > 12 &&
imageBuffer.toString("ascii", 0, 4) === "RIFF" &&
imageBuffer.toString("ascii", 8, 12) === "WEBP";
if ((mime === "image/png" && !isPng) ||
((mime === "image/jpeg" || mime === "image/jpg") && !isJpeg) ||
(mime === "image/webp" && !isWebp)) {
throw new Error("Generated image payload did not match its declared image format.");
}
const createdAt = new Date().toISOString();
const stamp = createdAt.replace(/[:.]/g, "-");
const seed = metadata.seed !== undefined && metadata.seed !== null ? `-${safeOutputName(metadata.seed)}` : "";
@@ -1623,7 +2067,7 @@ function saveGeneratedOutput(imageDataUrl, metadata = {}) {
const imagePath = path.join(OUTPUTS, imageFilename);
const metadataPath = path.join(OUTPUTS, metadataFilename);
fs.writeFileSync(imagePath, Buffer.from(match[2], "base64"));
fs.writeFileSync(imagePath, imageBuffer);
const savedMetadata = {
...metadata,
createdAt,
@@ -1789,9 +2233,9 @@ const server = http.createServer(async (req, res) => {
// GET /api/backend-status
if (req.url === "/api/backend-status" && req.method === "GET") {
return json(res, 200, {
ready: backendReady,
running: backendProc !== null,
return json(res, 200, {
ready: backendReady || openvinoReady,
running: backendProc !== null || openvinoProc !== null,
port: PORT_BACKEND,
preferredPort: PREFERRED_BACKEND_PORT,
error: backendError,
@@ -1828,6 +2272,17 @@ const server = http.createServer(async (req, res) => {
} catch (_) { return json(res, 200, { models: [] }); }
}
if (req.url === "/api/openvino-models" && req.method === "GET") {
const npuInfo = getOpenVinoNpuInfo();
return json(res, 200, {
supported: npuInfo.supported,
reason: npuInfo.reason || "",
npu: npuInfo.npu || null,
python: npuInfo.python || "",
models: npuInfo.supported ? getOpenVinoModelInfo() : [],
});
}
// POST /api/restart-backend — restart with new settings
if (req.url === "/api/restart-backend" && req.method === "POST") {
const body = await readJsonBody(req, res);
@@ -1836,11 +2291,13 @@ const server = http.createServer(async (req, res) => {
await killBackend();
await new Promise(r => setTimeout(r, 500));
const newSettings = {};
if (body.model) newSettings.model = path.join(MODELS, body.model);
if (body.model) newSettings.model = body.backend_type === "openvino-npu" ? String(body.model) : path.join(MODELS, body.model);
if (body.steps) newSettings.steps = parseInt(body.steps);
if (body.cfgScale) newSettings.cfgScale = parseFloat(body.cfgScale);
if (body.sampler) newSettings.sampler = body.sampler;
if (body.threads) newSettings.threads = parseInt(body.threads);
if (body.width) newSettings.width = parseInt(body.width);
if (body.height) newSettings.height = parseInt(body.height);
if (typeof body.use_gpu === "boolean") newSettings.useGpu = body.use_gpu;
if (body.backend_type) {
newSettings.backendType = String(body.backend_type);
@@ -1850,6 +2307,18 @@ const server = http.createServer(async (req, res) => {
if (typeof body.vae_on_cpu === "boolean") newSettings.vaeOnCpu = body.vae_on_cpu;
if (typeof body.flash_attn === "boolean") newSettings.flashAttn = body.flash_attn;
try {
if (newSettings.backendType === "openvino-npu") {
startBackend(newSettings).catch((err) => {
backendError = err.message || String(err);
backendLoadState = {
...backendLoadState,
active: false,
phase: "OpenVINO NPU model load failed",
};
console.error(" [openvino-npu] Startup failed:", backendError);
});
return json(res, 200, { ok: true, message: "OpenVINO NPU backend starting...", settings: currentSettings, port: PORT_BACKEND });
}
await startBackend(newSettings);
return json(res, 200, { ok: true, message: "Backend restarting...", settings: currentSettings, port: PORT_BACKEND });
} catch (err) {
@@ -1861,6 +2330,7 @@ const server = http.createServer(async (req, res) => {
// POST /api/stop-backend
if (req.url === "/api/stop-backend" && req.method === "POST") {
await killBackend();
await killOpenVinoWorker();
return json(res, 200, { ok: true });
}
@@ -1875,6 +2345,19 @@ const server = http.createServer(async (req, res) => {
return json(res, 200, { ok: true, message: "Download started" });
}
if (req.url === "/api/download-openvino-model" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
const modelId = String(body.model_id || body.modelId || "");
if (!modelId) return json(res, 400, { ok: false, error: "model_id is required" });
try {
startOpenVinoModelDownload(modelId);
return json(res, 200, { ok: true, message: "OpenVINO model download started" });
} catch (err) {
return json(res, 500, { ok: false, error: err.message || String(err) });
}
}
// GET /api/download-progress
if (req.url === "/api/download-progress" && req.method === "GET") {
return json(res, 200, downloadState);
@@ -1891,6 +2374,16 @@ const server = http.createServer(async (req, res) => {
return json(res, 200, generationState);
}
if (req.url === "/api/openvino-generate" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
try {
return json(res, 200, await generateWithOpenVino(body));
} catch (err) {
return json(res, 500, { ok: false, error: err.message || String(err) });
}
}
// GET /api/outputs
if (req.url === "/api/outputs" && req.method === "GET") {
return json(res, 200, { outputs: listGeneratedOutputs() });
@@ -1958,11 +2451,17 @@ const server = http.createServer(async (req, res) => {
const { filename } = body;
if (!filename) return json(res, 400, { error: "Filename is required" });
const openvinoModel = findOpenVinoModel(filename);
const safeFilename = path.basename(filename);
const modelPath = path.join(MODELS, safeFilename);
try {
if (fs.existsSync(modelPath)) {
if (openvinoModel?.installed && openvinoModel.path && pathInside(openvinoModel.path, OPENVINO_MODELS)) {
fs.rmSync(openvinoModel.path, { recursive: true, force: true });
cachedBackendOptions = null;
console.log(` [api] Deleted OpenVINO model: ${openvinoModel.id}`);
return json(res, 200, { ok: true, message: `Deleted ${openvinoModel.name}` });
} else if (fs.existsSync(modelPath)) {
fs.unlinkSync(modelPath);
console.log(` [api] Deleted model file: ${safeFilename}`);
return json(res, 200, { ok: true, message: `Deleted ${safeFilename}` });
+46
View File
@@ -0,0 +1,46 @@
# Optional OpenVINO NPU setup for Intel AI Boost systems.
# This does not modify the existing stable-diffusion.cpp backends.
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent $scriptDir
$venvDir = Join-Path $rootDir "app\tools\openvino-venv-win"
$pythonExe = Join-Path $venvDir "Scripts\python.exe"
Write-Host ""
Write-Host " ============================================================"
Write-Host " Local AI Image Generator - OpenVINO NPU Setup"
Write-Host " ============================================================"
Write-Host ""
$npu = Get-CimInstance Win32_PnPEntity -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like "*Intel(R) AI Boost*" -or ($_.Name -match "NPU" -and $_.PNPClass -eq "ComputeAccelerator") } |
Select-Object -First 1
if (-not $npu) {
Write-Host " [ERROR] Intel AI Boost NPU was not detected on this system." -ForegroundColor Red
exit 1
}
Write-Host " Detected: $($npu.Name)"
if (-not (Test-Path $pythonExe)) {
Write-Host " Creating Python environment: $venvDir"
python -m venv $venvDir
}
Write-Host " Installing OpenVINO GenAI runtime..."
& $pythonExe -m pip install --upgrade pip
& $pythonExe -m pip install openvino openvino-genai pillow huggingface-hub
Write-Host ""
Write-Host " Verifying OpenVINO NPU device..."
& $pythonExe -c "import openvino as ov; core=ov.Core(); print('Available devices:', core.available_devices); raise SystemExit(0 if 'NPU' in core.available_devices else 2)"
Write-Host ""
Write-Host " ============================================================"
Write-Host " OpenVINO NPU setup complete."
Write-Host " Restart windows.bat, then download an OpenVINO NPU model."
Write-Host " ============================================================"
Write-Host ""
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
#
# Optional OpenVINO NPU setup for Linux Intel Core Ultra systems.
# The Intel NPU kernel/user-mode driver must already be installed.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
VENV_DIR="$ROOT_DIR/app/tools/openvino-venv-linux"
PYTHON_BIN="$VENV_DIR/bin/python"
echo ""
echo " ============================================================"
echo " Local AI Image Generator - Linux OpenVINO NPU Setup"
echo " ============================================================"
echo ""
if [[ "$(uname -s)" != "Linux" ]]; then
echo " [ERROR] This setup script is for Linux only." >&2
exit 1
fi
if [[ "$(uname -m)" != "x86_64" ]]; then
echo " [ERROR] OpenVINO NPU support requires x86_64 Linux." >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo " [ERROR] python3 is required. Install Python 3 and the venv module." >&2
exit 1
fi
KERNEL_VERSION="$(uname -r)"
KERNEL_MAJOR="${KERNEL_VERSION%%.*}"
KERNEL_REST="${KERNEL_VERSION#*.}"
KERNEL_MINOR="${KERNEL_REST%%.*}"
if (( KERNEL_MAJOR < 6 || (KERNEL_MAJOR == 6 && KERNEL_MINOR < 6) )); then
echo " [ERROR] Intel NPU support requires Linux kernel 6.6 or newer."
echo " Current kernel: $KERNEL_VERSION"
exit 1
fi
if [[ ! -e /dev/accel/accel0 ]]; then
echo " [ERROR] Intel NPU device /dev/accel/accel0 was not found."
echo " Install the Intel Linux NPU driver and reboot first."
echo " Driver: https://github.com/intel/linux-npu-driver"
exit 1
fi
if [[ ! -x "$PYTHON_BIN" ]]; then
echo " Creating Python environment: $VENV_DIR"
if ! python3 -m venv "$VENV_DIR"; then
echo " [ERROR] Could not create the virtual environment."
echo " On Ubuntu, install python3-venv and retry."
exit 1
fi
fi
echo " Installing OpenVINO GenAI runtime..."
"$PYTHON_BIN" -m pip install --upgrade pip
"$PYTHON_BIN" -m pip install openvino openvino-genai pillow huggingface-hub
echo ""
echo " Verifying OpenVINO NPU device..."
"$PYTHON_BIN" - <<'PY'
import openvino as ov
core = ov.Core()
devices = core.available_devices
print(" Available devices:", ", ".join(devices))
if "NPU" not in devices:
raise SystemExit(
" [ERROR] OpenVINO is installed, but NPU is unavailable. "
"Verify the Intel NPU driver and device permissions."
)
print(" NPU:", core.get_property("NPU", "FULL_DEVICE_NAME"))
PY
echo ""
echo " ============================================================"
echo " OpenVINO NPU setup complete."
echo " Run ./linux.sh, then download an OpenVINO NPU model."
echo " ============================================================"
echo ""
+24 -2
View File
@@ -47,6 +47,18 @@ function Format-Speed {
return "{0:N0} KB/s" -f ($bps / 1KB)
}
function Enable-Tls12 {
try {
# Use the protocol's numeric value because older .NET enum definitions
# do not expose the Tls12 member even when Windows supports TLS 1.2.
$tls12 = [Enum]::ToObject([Net.SecurityProtocolType], 3072)
[Net.ServicePointManager]::SecurityProtocol =
[Net.ServicePointManager]::SecurityProtocol -bor $tls12
} catch {
throw "TLS 1.2 could not be enabled. This setup requires 64-bit Windows 10 or Windows 11 with current system updates. $($_.Exception.Message)"
}
}
function Invoke-RichDownload {
param([string]$Url, [string]$Dest, [string]$Label)
Print-Info "Downloading: $Label"
@@ -57,7 +69,7 @@ function Invoke-RichDownload {
$lastTime = [DateTime]::Now
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Enable-Tls12
$req = [System.Net.HttpWebRequest]::Create($Url)
$req.UserAgent = "Mozilla/5.0"
$resp = $req.GetResponse()
@@ -388,6 +400,16 @@ if (-not (Test-Path $npmCmd)) {
Read-Host; exit 1
}
# If node_modules is a Unix symlink/junction, remove it so Windows can install natively into a real directory
$nodeModulesDir = Join-Path $frontendDir "node_modules"
if (Test-Path $nodeModulesDir) {
$item = Get-Item $nodeModulesDir
if ($item.Attributes -match "ReparsePoint") {
Print-Info "Removing Unix symlink for node_modules on Windows..."
Remove-Item $nodeModulesDir -Force -ErrorAction SilentlyContinue
}
}
Push-Location $frontendDir
$oldPath = $env:PATH
try {
@@ -419,7 +441,7 @@ try {
# ── Done ─────────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host " ============================================================" -ForegroundColor Green
Write-Host " Setup complete! Just double-click start.bat to launch." -ForegroundColor Green
Write-Host " Setup complete! Just double-click windows.bat to launch." -ForegroundColor Green
Write-Host " ============================================================" -ForegroundColor Green
Write-Host ""
Read-Host " Press Enter to close..."
+24 -1
View File
@@ -361,6 +361,25 @@ if [[ ! -x "$NPM_BIN" ]]; then
exit 1
fi
# Ensure correct OS-specific node_modules folder is symlinked to avoid conflicts
FRONTEND_NODE_MODULES="$FRONTEND_DIR/node_modules"
if [[ "$PLATFORM" == "Darwin" ]]; then
OS_NODE_MODULES="$FRONTEND_DIR/node_modules_mac"
OS_LABEL="node_modules_mac"
else
OS_NODE_MODULES="$FRONTEND_DIR/node_modules_linux"
OS_LABEL="node_modules_linux"
fi
if [[ -d "$FRONTEND_NODE_MODULES" && ! -L "$FRONTEND_NODE_MODULES" ]]; then
print_info "Migrating existing node_modules to $OS_LABEL..."
mv "$FRONTEND_NODE_MODULES" "$OS_NODE_MODULES"
fi
rm -f "$FRONTEND_NODE_MODULES"
mkdir -p "$OS_NODE_MODULES"
ln -sf "$OS_LABEL" "$FRONTEND_NODE_MODULES"
cd "$FRONTEND_DIR"
export PATH="$NODE_DIR/bin:$PATH"
@@ -384,7 +403,11 @@ fi
# ── Done ────────────────────────────────────────────────────────────────────
echo ""
echo " ============================================================"
echo " Setup complete! Run ./start.sh to launch."
if [[ "$PLATFORM" == "Darwin" ]]; then
echo " Setup complete! Run ./mac.sh to launch."
else
echo " Setup complete! Run ./linux.sh to launch."
fi
echo " ============================================================"
echo ""
View File
File renamed without changes.