implement modular backend setup scripts, new frontend components, and expanded server services

This commit is contained in:
techjarves committed 2026-06-22 10:10:29 +05:30
1 parent bd89699994
commit ffec37fd0a
27 files changed
+309 -149

No files matched your search

+23 -24
View File
@@ -73,24 +73,24 @@ The normal platform setup installs both image and text backends. Text and image
## 📁 Repository Structure
```
local-ai-image-generator/
├── 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
├── README.md
├── scripts/
│ ├── setup.ps1 # Automated GPU-detect and environment installer (Windows)
│ ├── setup.sh # Automated GPU-detect and environment installer (Linux/macOS)
│ ├── reset.ps1 # Cleans runtime environments (Windows)
│ ├── reset.sh # Cleans runtime environments (Linux/macOS)
│ └── serve.cjs # UI web server and backend lifecycle manager
└── app/
├── frontend/ # UI source code (Vite + React)
├── models/ # Place weights here (.safetensors, .gguf, .ckpt)
└── outputs/ # Saved images and parameters metadata
```
|-- 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
|-- README.md
|-- scripts/
| |-- setup/ # Platform setup and backend installers
| |-- reset/ # Runtime cleanup scripts
| |-- server/ # UI server and backend lifecycle manager
| |-- workers/ # Local worker processes
| |-- build/ # Optional source build helpers
| `-- config/ # Runtime configuration catalogs
`-- app/
|-- frontend/ # UI source code (Vite + React)
|-- models/ # Place weights here (.safetensors, .gguf, .ckpt)
`-- outputs/ # Saved images and parameters metadata
---
@@ -157,9 +157,9 @@ Typical generation times for an image with **20 steps** (e.g. 512x512 resolution
---
## 🛠️ Troubleshooting
* **Reset Environment:** If a build fails or you want to clear dependencies, run `scripts/reset.ps1` (Windows) or `scripts/reset.sh` (Linux/macOS). (This preserves your models and generated images).
* **Port Conflicts:** The frontend uses `1420` by default. The backend tries `8080` first, then automatically falls back to a free port if `8080` is already busy.
* **Reset Environment:** If a build fails or you want to clear dependencies, run `scripts/reset/reset.ps1` (Windows) or `scripts/reset/reset.sh` (Linux/macOS). (This preserves your models and generated images).
* **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).
* **Port Conflicts:** The frontend uses `1420` by default. The backend tries `8080` first, then automatically falls back to a free port if `8080` is already busy.
* **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 `./linux.sh` or `./mac.sh` for the exact error (common causes are glibc mismatch, missing Vulkan drivers, or out-of-memory).
@@ -168,9 +168,9 @@ Typical generation times for an image with **20 steps** (e.g. 512x512 resolution
## 🔨 Building Linux Backends From Source
The setup script (`scripts/setup.sh`) now automates building and setting up the CUDA backend from source when selected. If you want to manually build all backends (CPU, Vulkan, and CUDA) at once, you can run the included `scripts/build_from_source.sh` script.
The setup script (`scripts/setup/setup.sh`) now automates building and setting up the CUDA backend from source when selected. If you want to manually build all backends (CPU, Vulkan, and CUDA) at once, you can run the included `scripts/build/build_from_source.sh` script.
For macOS, the included `scripts/build_from_source.sh` builds the Metal backend and copies it to `app/backend/mac/sd`.
For macOS, the included `scripts/build/build_from_source.sh` builds the Metal backend and copies it to `app/backend/mac/sd`.
### Requirements
- `git`, `cmake`, `make` (or `ninja`), and a C++17 compiler (`g++` / `clang++`).
@@ -210,8 +210,7 @@ cmake --build . --config Release -j$(getconf _NPROCESSORS_ONLN 2>/dev/null || sy
cp bin/sd* /path/to/Local-AI-Image-Generator/app/backend/linux/<backend>/
```
After copying, rename the server binary to match what `scripts/serve.cjs` expects:
- CPU: `sd` → `sd-cpu`
After copying, rename the server binary to match what `scripts/server/serve.cjs` expects:
- Vulkan: `sd` → `sd-vulkan`
- ROCm: `sd` → `sd-rocm`
+2
View File
@@ -169,6 +169,7 @@ function App() {
return {
language: parsed.language || "auto",
threads: Math.max(1, Math.min(32, Number(parsed.threads) || Math.max(1, Math.min(8, navigator.hardwareConcurrency || 4)))),
backendPreference: ["auto", "vulkan", "metal", "cpu"].includes(parsed.backendPreference) ? parsed.backendPreference : "auto",
translate: parsed.translate === true,
};
} catch (_) {}
@@ -176,6 +177,7 @@ function App() {
return {
language: "auto",
threads: Math.max(1, Math.min(8, navigator.hardwareConcurrency || 4)),
backendPreference: "auto",
translate: false,
};
});
+18 -1
View File
@@ -964,7 +964,7 @@ function Settings({
<SectionHeader
icon={Volume2}
title="Speech Transcription"
count={3}
count={4}
color="#10b981"
isExpanded={expandedSections.speech}
onToggle={() => toggleSection("speech")}
@@ -982,6 +982,23 @@ function Settings({
Transcription Settings
</div>
<div className="m3-field-group">
<div className="m3-text-field">
<label className="m3-text-field-label">Default Backend</label>
<select
className="m3-input"
value={speechSettings.backendPreference || "auto"}
onChange={(e) => updateSpeechSetting("backendPreference", e.target.value)}
>
<option value="auto">Auto - GPU if installed</option>
<option value="vulkan">Vulkan GPU</option>
<option value="metal">Metal GPU</option>
<option value="cpu">CPU</option>
</select>
<span className="settings-option-desc" style={{ marginTop: "4px", display: "block" }}>
Auto uses a GPU whisper.cpp backend when its binary exists, then falls back to CPU.
</span>
</div>
<div className="m3-text-field">
<label className="m3-text-field-label">Default Language</label>
<select
@@ -145,10 +145,12 @@ export default function SpeechTranscriber({
const language = speechSettings?.language || "auto";
const threads = speechSettings?.threads || 4;
const backendPreference = speechSettings?.backendPreference || "auto";
const translate = speechSettings?.translate === true;
const setLanguage = (val) => setSpeechSettings((prev) => ({ ...prev, language: val }));
const setThreads = (val) => setSpeechSettings((prev) => ({ ...prev, threads: val }));
const setBackendPreference = (val) => setSpeechSettings((prev) => ({ ...prev, backendPreference: val }));
const [audioBlob, setAudioBlob] = useState(null);
const [audioName, setAudioName] = useState("");
@@ -163,6 +165,9 @@ export default function SpeechTranscriber({
const abortRef = useRef(null);
const installedModels = useMemo(() => models.filter((model) => model.installed), [models]);
const isSelectedRuntimeLoaded = status.ready &&
status.settings?.model === selectedModel &&
(status.settings?.backendPreference || "auto") === backendPreference;
const isEnglishOnly = useMemo(() => {
if (!selectedModel) return false;
@@ -237,8 +242,8 @@ export default function SpeechTranscriber({
if (!selectedModel) return;
setIsLoadingModel(true);
try {
if (!status.ready || status.settings?.model !== selectedModel) {
await startSpeech(selectedModel, { language, threads });
if (!isSelectedRuntimeLoaded) {
await startSpeech(selectedModel, { language, threads, backendPreference });
}
await refresh();
} catch (err) {
@@ -300,10 +305,10 @@ export default function SpeechTranscriber({
const controller = new AbortController();
abortRef.current = controller;
try {
if (!status.ready || status.settings?.model !== selectedModel) {
if (!isSelectedRuntimeLoaded) {
setIsLoadingModel(true);
try {
await startSpeech(selectedModel, { language, threads });
await startSpeech(selectedModel, { language, threads, backendPreference });
await refresh();
} finally {
setIsLoadingModel(false);
@@ -313,6 +318,7 @@ export default function SpeechTranscriber({
model: selectedModel,
language,
threads,
backendPreference,
translate,
filename: audioName || "audio.wav",
signal: controller.signal,
@@ -386,6 +392,17 @@ export default function SpeechTranscriber({
</label>
<div className="speech-controls-row">
<label className="speech-label">
Backend
<select className="m3-input" value={backendPreference} onChange={(event) => setBackendPreference(event.target.value)}>
<option value="auto">Auto (GPU if installed)</option>
{(status.backends || []).map((backend) => (
<option key={backend.key} value={backend.key}>
{backend.label}{backend.installed ? " - installed" : " - missing"}
</option>
))}
</select>
</label>
<label className="speech-label">
Language
<select className="m3-input" value={language} onChange={(event) => setLanguage(event.target.value)}>
@@ -420,10 +437,10 @@ export default function SpeechTranscriber({
<button
className="m3-btn m3-btn-filled"
onClick={handleLoadSpeechModel}
disabled={!selectedModel || isLoadingModel || isTranscribing || (status.ready && status.settings?.model === selectedModel)}
disabled={!selectedModel || isLoadingModel || isTranscribing || isSelectedRuntimeLoaded}
>
{isLoadingModel ? <LoaderCircle className="progress-spinner" size={14} /> : <Play size={14} />}
<span>{status.ready && status.settings?.model === selectedModel ? "Loaded" : isLoadingModel ? "Loading" : "Load"}</span>
<span>{isSelectedRuntimeLoaded ? "Loaded" : isLoadingModel ? "Loading" : "Load"}</span>
</button>
<button className="m3-btn m3-btn-outlined" onClick={refresh}>
<Play size={14} />
+2
View File
@@ -593,6 +593,7 @@ export async function startSpeech(model, options = {}) {
model,
language: options.language || "auto",
threads: options.threads,
backendPreference: options.backendPreference || "auto",
}),
});
return await readJsonResponse(res, "The local server returned an invalid speech start response.");
@@ -609,6 +610,7 @@ export async function transcribeSpeech(fileOrBlob, options = {}) {
if (options.language) params.set("language", options.language);
if (options.filename) params.set("filename", options.filename);
if (options.threads) params.set("threads", String(options.threads));
if (options.backendPreference) params.set("backendPreference", options.backendPreference);
if (options.translate) params.set("translate", "true");
const res = await fetch(`/api/speech/transcribe?${params.toString()}`, {
+4 -4
View File
@@ -23,8 +23,8 @@ 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"
SETUP_SCRIPT="$SCRIPT_DIR/scripts/setup/setup.sh"
SERVE_SCRIPT="$SCRIPT_DIR/scripts/server/serve.cjs"
FRONTEND_PORT="${FRONTEND_PORT:-1420}"
LLM_PORT="${LLM_PORT:-10086}"
@@ -51,7 +51,7 @@ for arg in "$@"; do
done
if [[ $SETUP_OPENVINO -eq 1 ]]; then
bash "$SCRIPT_DIR/scripts/setup-openvino-npu.sh"
bash "$SCRIPT_DIR/scripts/setup/setup-openvino-npu.sh"
fi
# ── Setup node_modules to avoid OS conflicts ────────────────────────────────
@@ -133,7 +133,7 @@ LLM_ROCM_PATH="$APP_DIR/llm-backend/linux/rocm/llama-server"
LLM_SYCL_PATH="$APP_DIR/llm-backend/linux/sycl/llama-server"
LLM_VULKAN_PATH="$APP_DIR/llm-backend/linux/vulkan/llama-server"
LLM_CPU_PATH="$APP_DIR/llm-backend/linux/cpu/llama-server"
SPEECH_BACKEND_PATH="$APP_DIR/speech-backend/linux/whisper-cli"
SPEECH_BACKEND_PATH="$APP_DIR/speech-backend/linux/cpu/whisper-cli"
TTS_RUNTIME_PATH="$APP_DIR/tts-runtime/node_modules/kokoro-js"
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."
+2 -2
View File
@@ -27,8 +27,8 @@ TTS_RUNTIME_PATH="$APP_DIR/tts-runtime/node_modules/kokoro-js"
PLATFORM_LABEL="macOS"
DIST_INDEX="$APP_DIR/dist/index.html"
SETUP_SCRIPT="$SCRIPT_DIR/scripts/setup.sh"
SERVE_SCRIPT="$SCRIPT_DIR/scripts/serve.cjs"
SETUP_SCRIPT="$SCRIPT_DIR/scripts/setup/setup.sh"
SERVE_SCRIPT="$SCRIPT_DIR/scripts/server/serve.cjs"
FRONTEND_PORT="${FRONTEND_PORT:-1420}"
LLM_PORT="${LLM_PORT:-10086}"
+1 -1
View File
@@ -7,7 +7,7 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
APP_DIR="$ROOT_DIR/app"
PLATFORM="$(uname -s)"
if [ "$PLATFORM" = "Darwin" ]; then
File renamed without changes.
@@ -1,8 +1,8 @@
# scripts/reset.ps1
# scripts/reset/reset.ps1
# Resets portable app dependencies/builds while preserving user models and outputs.
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent $scriptDir
$rootDir = Split-Path -Parent (Split-Path -Parent $scriptDir)
$appDir = Join-Path $rootDir "app"
Write-Host ""
+1 -1
View File
@@ -7,7 +7,7 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
APP_DIR="$ROOT_DIR/app"
echo ""
+109 -25
View File
@@ -38,7 +38,7 @@ let PORT_SPEECH = PREFERRED_SPEECH_PORT;
let PORT_TTS = PREFERRED_TTS_PORT;
const MAX_JSON_BODY_BYTES = 64 * 1024 * 1024;
const SERVER_BUILD = "text-image-v1";
const ROOT = path.join(__dirname, "..");
const ROOT = path.join(__dirname, "..", "..");
const DIST = path.join(ROOT, "app", "dist");
const osPlatform = process.platform;
const BACKEND_PATHS = {
@@ -130,12 +130,25 @@ if (!fs.existsSync(TTS_CACHE)) {
fs.mkdirSync(TTS_CACHE, { recursive: true });
}
const TTS_RUNTIME = path.join(ROOT, "app", "tts-runtime");
const TTS_WORKER = path.join(ROOT, "scripts", "tts-kokoro-worker.mjs");
const TTS_WORKER = path.join(ROOT, "scripts", "workers", "tts-kokoro-worker.mjs");
const SPEECH_BACKEND_ROOT = path.join(ROOT, "app", "speech-backend");
const SPEECH_BACKEND_PATHS = {
winVulkanCli: path.join(SPEECH_BACKEND_ROOT, "win", "vulkan", "whisper-cli.exe"),
winVulkanServer: path.join(SPEECH_BACKEND_ROOT, "win", "vulkan", "whisper-server.exe"),
winCpuCli: path.join(SPEECH_BACKEND_ROOT, "win", "cpu", "whisper-cli.exe"),
winCpuServer: path.join(SPEECH_BACKEND_ROOT, "win", "cpu", "whisper-server.exe"),
winCli: path.join(ROOT, "app", "speech-backend", "win", "whisper-cli.exe"),
winServer: path.join(ROOT, "app", "speech-backend", "win", "whisper-server.exe"),
linuxVulkanCli: path.join(SPEECH_BACKEND_ROOT, "linux", "vulkan", "whisper-cli"),
linuxVulkanServer: path.join(SPEECH_BACKEND_ROOT, "linux", "vulkan", "whisper-server"),
linuxCpuCli: path.join(SPEECH_BACKEND_ROOT, "linux", "cpu", "whisper-cli"),
linuxCpuServer: path.join(SPEECH_BACKEND_ROOT, "linux", "cpu", "whisper-server"),
linuxCli: path.join(ROOT, "app", "speech-backend", "linux", "whisper-cli"),
linuxServer: path.join(ROOT, "app", "speech-backend", "linux", "whisper-server"),
macMetalCli: path.join(SPEECH_BACKEND_ROOT, "mac", "metal", "whisper-cli"),
macMetalServer: path.join(SPEECH_BACKEND_ROOT, "mac", "metal", "whisper-server"),
macCpuCli: path.join(SPEECH_BACKEND_ROOT, "mac", "cpu", "whisper-cli"),
macCpuServer: path.join(SPEECH_BACKEND_ROOT, "mac", "cpu", "whisper-server"),
macCli: path.join(ROOT, "app", "speech-backend", "mac", "whisper-cli"),
macServer: path.join(ROOT, "app", "speech-backend", "mac", "whisper-server"),
};
@@ -227,6 +240,7 @@ let speechSettings = {
model: null,
language: "auto",
threads: Math.max(1, Math.min(8, os.cpus().length || 4)),
backendPreference: "auto",
backendBinary: "",
backendMode: "",
};
@@ -1391,8 +1405,8 @@ function getOpenVinoNpuInfo() {
const python = getOpenVinoPython();
if (!python) {
const setupScript = osPlatform === "win32"
? "scripts/setup-openvino-npu.ps1"
: "bash scripts/setup-openvino-npu.sh";
? "scripts/setup/setup-openvino-npu.ps1"
: "bash scripts/setup/setup-openvino-npu.sh";
cachedOpenVinoNpuInfo = {
supported: false,
reason: `OpenVINO GenAI runtime is not installed. Run ${setupScript} first.`,
@@ -1669,25 +1683,86 @@ function findExistingFile(candidates) {
return candidates.find((candidate) => candidate && fs.existsSync(candidate)) || "";
}
function getSpeechBackend() {
function getSpeechBackendCandidates() {
if (osPlatform === "win32") {
return {
cli: findExistingFile([SPEECH_BACKEND_PATHS.winCli, path.join(ROOT, "app", "speech-backend", "win", "main.exe")]),
server: findExistingFile([SPEECH_BACKEND_PATHS.winServer, path.join(ROOT, "app", "speech-backend", "win", "server.exe")]),
mode: "whisper.cpp CPU",
};
return [
{
key: "vulkan",
label: "Vulkan GPU",
mode: "whisper.cpp Vulkan GPU",
cli: findExistingFile([SPEECH_BACKEND_PATHS.winVulkanCli, path.join(SPEECH_BACKEND_ROOT, "win", "vulkan", "main.exe")]),
server: findExistingFile([SPEECH_BACKEND_PATHS.winVulkanServer, path.join(SPEECH_BACKEND_ROOT, "win", "vulkan", "server.exe")]),
pathHint: path.join(SPEECH_BACKEND_ROOT, "win", "vulkan"),
},
{
key: "cpu",
label: "CPU",
mode: "whisper.cpp CPU",
cli: findExistingFile([SPEECH_BACKEND_PATHS.winCpuCli, SPEECH_BACKEND_PATHS.winCli, path.join(SPEECH_BACKEND_ROOT, "win", "cpu", "main.exe"), path.join(SPEECH_BACKEND_ROOT, "win", "main.exe")]),
server: findExistingFile([SPEECH_BACKEND_PATHS.winCpuServer, SPEECH_BACKEND_PATHS.winServer, path.join(SPEECH_BACKEND_ROOT, "win", "cpu", "server.exe"), path.join(SPEECH_BACKEND_ROOT, "win", "server.exe")]),
pathHint: path.join(SPEECH_BACKEND_ROOT, "win", "cpu"),
},
];
}
if (osPlatform === "darwin") {
return {
cli: findExistingFile([SPEECH_BACKEND_PATHS.macCli, path.join(ROOT, "app", "speech-backend", "mac", "main")]),
server: findExistingFile([SPEECH_BACKEND_PATHS.macServer, path.join(ROOT, "app", "speech-backend", "mac", "server")]),
mode: "whisper.cpp Metal/CPU",
};
return [
{
key: "metal",
label: "Metal GPU",
mode: "whisper.cpp Metal GPU",
cli: findExistingFile([SPEECH_BACKEND_PATHS.macMetalCli, path.join(SPEECH_BACKEND_ROOT, "mac", "metal", "main")]),
server: findExistingFile([SPEECH_BACKEND_PATHS.macMetalServer, path.join(SPEECH_BACKEND_ROOT, "mac", "metal", "server")]),
pathHint: path.join(SPEECH_BACKEND_ROOT, "mac", "metal"),
},
{
key: "cpu",
label: "CPU",
mode: "whisper.cpp CPU",
cli: findExistingFile([SPEECH_BACKEND_PATHS.macCpuCli, SPEECH_BACKEND_PATHS.macCli, path.join(SPEECH_BACKEND_ROOT, "mac", "cpu", "main"), path.join(SPEECH_BACKEND_ROOT, "mac", "main")]),
server: findExistingFile([SPEECH_BACKEND_PATHS.macCpuServer, SPEECH_BACKEND_PATHS.macServer, path.join(SPEECH_BACKEND_ROOT, "mac", "cpu", "server"), path.join(SPEECH_BACKEND_ROOT, "mac", "server")]),
pathHint: path.join(SPEECH_BACKEND_ROOT, "mac", "cpu"),
},
];
}
return [
{
key: "vulkan",
label: "Vulkan GPU",
mode: "whisper.cpp Vulkan GPU",
cli: findExistingFile([SPEECH_BACKEND_PATHS.linuxVulkanCli, path.join(SPEECH_BACKEND_ROOT, "linux", "vulkan", "main")]),
server: findExistingFile([SPEECH_BACKEND_PATHS.linuxVulkanServer, path.join(SPEECH_BACKEND_ROOT, "linux", "vulkan", "server")]),
pathHint: path.join(SPEECH_BACKEND_ROOT, "linux", "vulkan"),
},
{
key: "cpu",
label: "CPU",
mode: "whisper.cpp CPU",
cli: findExistingFile([SPEECH_BACKEND_PATHS.linuxCpuCli, SPEECH_BACKEND_PATHS.linuxCli, path.join(SPEECH_BACKEND_ROOT, "linux", "cpu", "main"), path.join(SPEECH_BACKEND_ROOT, "linux", "main")]),
server: findExistingFile([SPEECH_BACKEND_PATHS.linuxCpuServer, SPEECH_BACKEND_PATHS.linuxServer, path.join(SPEECH_BACKEND_ROOT, "linux", "cpu", "server"), path.join(SPEECH_BACKEND_ROOT, "linux", "server")]),
pathHint: path.join(SPEECH_BACKEND_ROOT, "linux", "cpu"),
},
];
}
function normalizeSpeechBackendPreference(value) {
const raw = String(value || "auto").trim().toLowerCase();
if (["auto", "cpu", "vulkan", "metal"].includes(raw)) return raw;
return "auto";
}
function getSpeechBackend(preference = speechSettings.backendPreference) {
const candidates = getSpeechBackendCandidates().map((candidate) => ({
...candidate,
installed: Boolean(candidate.cli),
}));
const preferred = normalizeSpeechBackendPreference(preference);
const selected = preferred === "auto"
? candidates.find((candidate) => candidate.installed) || candidates[candidates.length - 1]
: candidates.find((candidate) => candidate.key === preferred) || candidates.find((candidate) => candidate.installed) || candidates[candidates.length - 1];
return {
cli: findExistingFile([SPEECH_BACKEND_PATHS.linuxCli, path.join(ROOT, "app", "speech-backend", "linux", "main")]),
server: findExistingFile([SPEECH_BACKEND_PATHS.linuxServer, path.join(ROOT, "app", "speech-backend", "linux", "server")]),
mode: "whisper.cpp CPU",
...(selected || { key: "cpu", label: "CPU", mode: "whisper.cpp CPU", cli: "", server: "" }),
preference: preferred,
candidates,
};
}
@@ -1846,7 +1921,7 @@ function runExclusiveTtsOperation(operation) {
async function startTts(settings = {}) {
const runtime = getTtsRuntimeStatus();
if (!runtime.installed) {
throw new Error("Kokoro TTS runtime is not installed. Run scripts/setup-tts for this platform.");
throw new Error("Kokoro TTS runtime is not installed. Run scripts/setup/setup-tts for this platform.");
}
const model = resolveTtsModel(settings.model || ttsSettings.model);
await killBackend();
@@ -2041,9 +2116,10 @@ function runExclusiveSpeechOperation(operation) {
}
async function startSpeech(settings = {}) {
const backend = getSpeechBackend();
const backendPreference = normalizeSpeechBackendPreference(settings.backendPreference || speechSettings.backendPreference);
const backend = getSpeechBackend(backendPreference);
if (!backend.cli) {
throw new Error("whisper.cpp is not installed. Run the platform setup script to install the speech backend.");
throw new Error(`${backend.label || "Selected"} whisper.cpp backend is not installed. Run setup or copy a compatible binary to ${backend.pathHint || "app/speech-backend"}.`);
}
const model = resolveSpeechModel(settings.model);
await killBackend();
@@ -2057,6 +2133,7 @@ async function startSpeech(settings = {}) {
model: model.filename,
language: settings.language || speechSettings.language || "auto",
threads: Math.max(1, Math.min(32, Number(settings.threads) || speechSettings.threads || 4)),
backendPreference,
backendBinary: path.basename(backend.cli),
backendMode: backend.mode,
};
@@ -2146,9 +2223,10 @@ function transcribeWavBuffer(buffer, options = {}) {
reject(new Error("Speech transcription currently accepts WAV audio only."));
return;
}
const backend = getSpeechBackend();
const backendPreference = normalizeSpeechBackendPreference(options.backendPreference || speechSettings.backendPreference);
const backend = getSpeechBackend(backendPreference);
if (!backend.cli) {
reject(new Error("whisper.cpp CLI backend is not installed."));
reject(new Error(`${backend.label || "Selected"} whisper.cpp CLI backend is not installed.`));
return;
}
const model = resolveSpeechModel(options.model || speechSettings.model);
@@ -2192,6 +2270,7 @@ function transcribeWavBuffer(buffer, options = {}) {
windowsHide: true,
env: {
...process.env,
...(osPlatform === "win32" ? { PATH: path.dirname(backend.cli) + (process.env.PATH ? `;${process.env.PATH}` : "") } : {}),
...(osPlatform === "linux" ? { LD_LIBRARY_PATH: path.dirname(backend.cli) + (process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : "") } : {}),
...(osPlatform === "darwin" ? { DYLD_LIBRARY_PATH: path.dirname(backend.cli) + (process.env.DYLD_LIBRARY_PATH ? `:${process.env.DYLD_LIBRARY_PATH}` : "") } : {}),
},
@@ -2229,6 +2308,8 @@ function transcribeWavBuffer(buffer, options = {}) {
model: model.filename,
modelName: model.name,
language,
backend: backend.key,
backendMode: backend.mode,
sourceFilename,
durationMs: Date.now() - startedAt,
raw,
@@ -2656,7 +2737,7 @@ function getCoreMLNpuInfo() {
return {
supported: true,
ready: false,
reason: "CoreML Python environment is not set up. Run scripts/setup-coreml-npu.sh first.",
reason: "CoreML Python environment is not set up. Run scripts/setup/setup-coreml-npu.sh first.",
};
}
@@ -3307,7 +3388,7 @@ async function startOpenVinoWorker(settings = {}) {
device: currentSettings.backendDevice,
};
const workerPath = path.join(ROOT, "scripts", "openvino_npu_worker.py");
const workerPath = path.join(ROOT, "scripts", "workers", "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, [
@@ -5030,6 +5111,8 @@ const server = http.createServer(async (req, res) => {
backendPath: backend.cli || "",
serverPath: backend.server || "",
backendMode: backend.mode,
backendPreference: backend.preference,
backends: backend.candidates,
error: speechError,
settings: speechSettings,
transcription: speechTranscriptionState,
@@ -5066,6 +5149,7 @@ const server = http.createServer(async (req, res) => {
language: parsed.searchParams.get("language") || "auto",
filename: parsed.searchParams.get("filename") || req.headers["x-filename"] || "recording.wav",
threads: parsed.searchParams.get("threads") || speechSettings.threads,
backendPreference: parsed.searchParams.get("backendPreference") || speechSettings.backendPreference,
translate: parsed.searchParams.get("translate") === "true",
}));
return json(res, 200, { ok: true, transcription: result });
-64
View File
@@ -1,64 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
APP_DIR="$ROOT_DIR/app"
TOOLS_DIR="$APP_DIR/tools"
RELEASE="${WHISPER_RELEASE:-v1.9.1}"
PLATFORM="$(uname -s)"
ARCH="$(uname -m)"
download_and_extract() {
local asset="$1"
local dest="$2"
local archive="$TOOLS_DIR/$asset"
local url="https://github.com/ggml-org/whisper.cpp/releases/download/$RELEASE/$asset"
if [[ -x "$dest/whisper-cli" ]]; then
echo " OK whisper.cpp speech backend already ready: $dest"
return 0
fi
mkdir -p "$TOOLS_DIR" "$dest"
rm -f "$archive" "$archive.part"
echo " >> Downloading $asset"
curl -fSL --progress-bar "$url" -o "$archive.part"
mv "$archive.part" "$archive"
tar -xzf "$archive" -C "$dest" --strip-components=1
rm -f "$archive"
chmod +x "$dest"/whisper-* "$dest"/main "$dest"/server 2>/dev/null || true
if [[ ! -x "$dest/whisper-cli" && -x "$dest/main" ]]; then
cp "$dest/main" "$dest/whisper-cli"
chmod +x "$dest/whisper-cli"
fi
if [[ ! -x "$dest/whisper-server" && -x "$dest/server" ]]; then
cp "$dest/server" "$dest/whisper-server"
chmod +x "$dest/whisper-server"
fi
if [[ ! -x "$dest/whisper-cli" ]]; then
echo " XX whisper-cli was not found after extracting $asset" >&2
return 1
fi
}
if [[ "$PLATFORM" == "Linux" ]]; then
if [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then
download_and_extract "whisper-bin-ubuntu-arm64.tar.gz" "$APP_DIR/speech-backend/linux"
else
download_and_extract "whisper-bin-ubuntu-x64.tar.gz" "$APP_DIR/speech-backend/linux"
fi
elif [[ "$PLATFORM" == "Darwin" ]]; then
mkdir -p "$APP_DIR/speech-backend/mac"
if [[ -x "$APP_DIR/speech-backend/mac/whisper-cli" ]]; then
echo " OK whisper.cpp macOS speech backend already ready."
else
echo " !! No official portable macOS whisper.cpp CLI archive is available for this setup script yet."
echo " Build whisper.cpp manually and copy whisper-cli to app/speech-backend/mac/whisper-cli."
fi
else
echo "Unsupported platform: $PLATFORM" >&2
exit 1
fi
+1 -1
View File
@@ -7,7 +7,7 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
VENV_DIR="$ROOT_DIR/app/backend/mac/coreml_venv"
PYTHON_BIN="$VENV_DIR/bin/python"
@@ -2,7 +2,7 @@ param([string]$Release = "b9668")
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent $scriptDir
$rootDir = Split-Path -Parent (Split-Path -Parent $scriptDir)
$appDir = Join-Path $rootDir "app"
$toolsDir = Join-Path $appDir "tools"
$llmRoot = Join-Path $appDir "llm-backend\win"
@@ -2,7 +2,7 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
APP_DIR="$ROOT_DIR/app"
TOOLS_DIR="$APP_DIR/tools"
RELEASE="${LLAMA_RELEASE:-b9668}"
@@ -4,7 +4,7 @@
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent $scriptDir
$rootDir = Split-Path -Parent (Split-Path -Parent $scriptDir)
$venvDir = Join-Path $rootDir "app\tools\openvino-venv-win"
$pythonExe = Join-Path $venvDir "Scripts\python.exe"
@@ -6,7 +6,7 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
VENV_DIR="$ROOT_DIR/app/tools/openvino-venv-linux"
PYTHON_BIN="$VENV_DIR/bin/python"
@@ -1,7 +1,7 @@
# Local AI Studio - Kokoro TTS setup for Windows
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent $scriptDir
$rootDir = Split-Path -Parent (Split-Path -Parent $scriptDir)
$appDir = Join-Path $rootDir "app"
$toolsDir = Join-Path $appDir "tools"
$nodeDir = Join-Path $toolsDir "node-win"
@@ -25,7 +25,7 @@ Write-Host ""
New-Item -ItemType Directory -Force -Path $runtimeDir, $modelsDir, $outputsDir, $cacheDir | Out-Null
if (-not (Test-Path $nodeExe) -or -not (Test-Path $npmCmd)) {
Print-Fail "Portable Node.js is missing. Run scripts/setup.ps1 first."
Print-Fail "Portable Node.js is missing. Run scripts/setup/setup.ps1 first."
exit 1
}
@@ -6,7 +6,7 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
APP_DIR="$ROOT_DIR/app"
TOOLS_DIR="$APP_DIR/tools"
PLATFORM="$(uname -s)"
@@ -37,7 +37,7 @@ echo ""
mkdir -p "$RUNTIME_DIR" "$MODELS_DIR" "$OUTPUTS_DIR" "$CACHE_DIR"
if [[ ! -x "$NODE_BIN" || ! -x "$NPM_BIN" ]]; then
print_fail "Portable Node.js is missing. Run scripts/setup.sh first."
print_fail "Portable Node.js is missing. Run scripts/setup/setup.sh first."
exit 1
fi
@@ -2,10 +2,12 @@ param([string]$Release = "v1.9.1")
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent $scriptDir
$rootDir = Split-Path -Parent (Split-Path -Parent $scriptDir)
$appDir = Join-Path $rootDir "app"
$toolsDir = Join-Path $appDir "tools"
$speechRoot = Join-Path $appDir "speech-backend\win"
$speechWinRoot = Join-Path $appDir "speech-backend\win"
$speechRoot = Join-Path $speechWinRoot "cpu"
$speechVulkanRoot = Join-Path $speechWinRoot "vulkan"
function Enable-Tls12 {
try {
@@ -41,11 +43,29 @@ function Find-FirstFile {
return $null
}
New-Item -ItemType Directory -Force -Path $toolsDir, $speechRoot | Out-Null
New-Item -ItemType Directory -Force -Path $toolsDir, $speechRoot, $speechVulkanRoot | Out-Null
$cliPath = Join-Path $speechRoot "whisper-cli.exe"
if (Test-Path $cliPath) {
Write-Host " OK whisper.cpp speech backend already ready."
if (-not (Test-Path (Join-Path $speechVulkanRoot "whisper-cli.exe"))) {
Write-Host " .. Optional Vulkan GPU backend slot: app/speech-backend/win/vulkan"
}
exit 0
}
$legacyCli = Join-Path $speechWinRoot "whisper-cli.exe"
$legacyServer = Join-Path $speechWinRoot "whisper-server.exe"
if (Test-Path $legacyCli) {
Copy-Item $legacyCli $cliPath -Force
if (Test-Path $legacyServer) {
Copy-Item $legacyServer (Join-Path $speechRoot "whisper-server.exe") -Force
}
Get-ChildItem $speechWinRoot -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notin @("whisper-cli.exe", "whisper-server.exe") } |
ForEach-Object { Copy-Item $_.FullName (Join-Path $speechRoot $_.Name) -Force }
Write-Host " OK migrated existing whisper.cpp CPU backend to app/speech-backend/win/cpu."
Write-Host " .. For GPU transcription, place a Vulkan-enabled whisper.cpp build in app/speech-backend/win/vulkan."
exit 0
}
@@ -81,3 +101,5 @@ if (-not (Test-Path $cliPath)) {
throw "whisper-cli.exe was not installed."
}
Write-Host " OK whisper.cpp speech backend installed."
Write-Host " .. CPU backend path: app/speech-backend/win/cpu"
Write-Host " .. Optional Vulkan GPU backend path: app/speech-backend/win/vulkan"
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
APP_DIR="$ROOT_DIR/app"
TOOLS_DIR="$APP_DIR/tools"
RELEASE="${WHISPER_RELEASE:-v1.9.1}"
PLATFORM="$(uname -s)"
ARCH="$(uname -m)"
download_and_extract() {
local asset="$1"
local dest="$2"
local archive="$TOOLS_DIR/$asset"
local url="https://github.com/ggml-org/whisper.cpp/releases/download/$RELEASE/$asset"
if [[ -x "$dest/whisper-cli" ]]; then
echo " OK whisper.cpp speech backend already ready: $dest"
return 0
fi
mkdir -p "$TOOLS_DIR" "$dest"
rm -f "$archive" "$archive.part"
echo " >> Downloading $asset"
curl -fSL --progress-bar "$url" -o "$archive.part"
mv "$archive.part" "$archive"
tar -xzf "$archive" -C "$dest" --strip-components=1
rm -f "$archive"
chmod +x "$dest"/whisper-* "$dest"/main "$dest"/server 2>/dev/null || true
if [[ ! -x "$dest/whisper-cli" && -x "$dest/main" ]]; then
cp "$dest/main" "$dest/whisper-cli"
chmod +x "$dest/whisper-cli"
fi
if [[ ! -x "$dest/whisper-server" && -x "$dest/server" ]]; then
cp "$dest/server" "$dest/whisper-server"
chmod +x "$dest/whisper-server"
fi
if [[ ! -x "$dest/whisper-cli" ]]; then
echo " XX whisper-cli was not found after extracting $asset" >&2
return 1
fi
}
if [[ "$PLATFORM" == "Linux" ]]; then
mkdir -p "$APP_DIR/speech-backend/linux/cpu" "$APP_DIR/speech-backend/linux/vulkan"
if [[ ! -x "$APP_DIR/speech-backend/linux/cpu/whisper-cli" && -x "$APP_DIR/speech-backend/linux/whisper-cli" ]]; then
cp "$APP_DIR/speech-backend/linux"/whisper-* "$APP_DIR/speech-backend/linux/cpu/" 2>/dev/null || true
cp "$APP_DIR/speech-backend/linux"/main "$APP_DIR/speech-backend/linux/cpu/" 2>/dev/null || true
cp "$APP_DIR/speech-backend/linux"/server "$APP_DIR/speech-backend/linux/cpu/" 2>/dev/null || true
chmod +x "$APP_DIR/speech-backend/linux/cpu"/whisper-* "$APP_DIR/speech-backend/linux/cpu"/main "$APP_DIR/speech-backend/linux/cpu"/server 2>/dev/null || true
echo " OK migrated existing whisper.cpp CPU backend to app/speech-backend/linux/cpu."
fi
if [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then
download_and_extract "whisper-bin-ubuntu-arm64.tar.gz" "$APP_DIR/speech-backend/linux/cpu"
else
download_and_extract "whisper-bin-ubuntu-x64.tar.gz" "$APP_DIR/speech-backend/linux/cpu"
fi
echo " .. CPU backend path: app/speech-backend/linux/cpu"
echo " .. Optional Vulkan GPU backend path: app/speech-backend/linux/vulkan"
elif [[ "$PLATFORM" == "Darwin" ]]; then
mkdir -p "$APP_DIR/speech-backend/mac/cpu" "$APP_DIR/speech-backend/mac/metal"
if [[ ! -x "$APP_DIR/speech-backend/mac/cpu/whisper-cli" && -x "$APP_DIR/speech-backend/mac/whisper-cli" ]]; then
cp "$APP_DIR/speech-backend/mac"/whisper-* "$APP_DIR/speech-backend/mac/cpu/" 2>/dev/null || true
cp "$APP_DIR/speech-backend/mac"/main "$APP_DIR/speech-backend/mac/cpu/" 2>/dev/null || true
cp "$APP_DIR/speech-backend/mac"/server "$APP_DIR/speech-backend/mac/cpu/" 2>/dev/null || true
chmod +x "$APP_DIR/speech-backend/mac/cpu"/whisper-* "$APP_DIR/speech-backend/mac/cpu"/main "$APP_DIR/speech-backend/mac/cpu"/server 2>/dev/null || true
echo " OK migrated existing whisper.cpp backend to app/speech-backend/mac/cpu."
fi
if [[ -x "$APP_DIR/speech-backend/mac/cpu/whisper-cli" || -x "$APP_DIR/speech-backend/mac/metal/whisper-cli" ]]; then
echo " OK whisper.cpp macOS speech backend already ready."
else
echo " !! No official portable macOS whisper.cpp CLI archive is available for this setup script yet."
echo " Build whisper.cpp manually and copy whisper-cli to app/speech-backend/mac/cpu or app/speech-backend/mac/metal."
fi
else
echo "Unsupported platform: $PLATFORM" >&2
exit 1
fi
@@ -1,8 +1,8 @@
# Local AI Image Generator - Setup Script
# scripts/ lives at root, app/ is a sibling folder
# scripts/setup/ lives under root, app/ is a root sibling of scripts/
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent $scriptDir
$rootDir = Split-Path -Parent (Split-Path -Parent $scriptDir)
$appDir = Join-Path $rootDir "app"
$frontendDir = Join-Path $appDir "frontend"
$toolsDir = Join-Path $appDir "tools"
+2 -2
View File
@@ -7,7 +7,7 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
APP_DIR="$ROOT_DIR/app"
FRONTEND_DIR="$APP_DIR/frontend"
TOOLS_DIR="$APP_DIR/tools"
@@ -680,6 +680,6 @@ echo ""
if [[ "$PLATFORM" == "Linux" && $MAX_PERF -eq 0 ]] && [[ "$VENDOR" == "nvidia" || "$VENDOR" == "amd" ]]; then
echo " Tip: For maximum GPU performance, re-run with:"
echo " ./scripts/setup.sh --max-perf"
echo " ./scripts/setup/setup.sh --max-perf"
echo ""
fi
File renamed without changes.
File renamed without changes.
+3 -3
View File
@@ -6,7 +6,7 @@ set APP=%~dp0app
set NODE=%APP%\tools\node-win\node.exe
set NPM=%APP%\tools\node-win\npm.cmd
set DIST=%APP%\dist\index.html
set SETUP=%~dp0scripts\setup.ps1
set SETUP=%~dp0scripts\setup\setup.ps1
set CUDA_BACKEND=%APP%\backend\win\cuda\sd-cuda.exe
set VULKAN_BACKEND=%APP%\backend\win\vulkan\sd-vulkan.exe
set LLM_CUDA_BACKEND=%APP%\llm-backend\win\cuda\llama-server.exe
@@ -14,9 +14,9 @@ set LLM_HIP_BACKEND=%APP%\llm-backend\win\hip\llama-server.exe
set LLM_VULKAN_BACKEND=%APP%\llm-backend\win\vulkan\llama-server.exe
set LLM_SYCL_BACKEND=%APP%\llm-backend\win\sycl\llama-server.exe
set LLM_CPU_BACKEND=%APP%\llm-backend\win\cpu\llama-server.exe
set SPEECH_BACKEND=%APP%\speech-backend\win\whisper-cli.exe
set SPEECH_BACKEND=%APP%\speech-backend\win\cpu\whisper-cli.exe
set TTS_RUNTIME=%APP%\tts-runtime\node_modules\kokoro-js
set SERVE=%~dp0scripts\serve.cjs
set SERVE=%~dp0scripts\server\serve.cjs
if "%FRONTEND_PORT%"=="" set FRONTEND_PORT=1420
if "%LLM_PORT%"=="" set LLM_PORT=10086
set SETUP_REASON=