Files
PaintPlus/frontend/src/js/services/api.js
T
Claude 9b895673eb feat: GPU capability display in UI + GTX 1060 6GB SDXL fix
Model selection:
- Add sdxl_offload tier (eff_vram ≥ 4.0 GB) for GTX 1060 6GB and Quadro
  6GB cards that were falling through to SD 2.1 despite SDXL fitting with
  model_cpu_offload. Cards with 5.3 GB effective VRAM now get SDXL quality.
- Update _tier_label(), _caps(), _build_warnings() for new tier.

Frontend GPU display:
- api.js: add getGpuStatus() fetching /api/gpu/status
- capabilities.js: add getGpuStatus() export with own LRU cache;
  refreshCapabilities() now also resets GPU status cache
- provider-badge.js: when AI_PROVIDER=local_gpu show green badge with
  GPU name, tier, VRAM, CC, feature flags, and capabilities in tooltip.
  Strip "NVIDIA GeForce" prefix so "GTX 1060 6GB" fits in badge.
- ai_provider_settings.js: add local_gpu to all provider dropdowns;
  show GPU info panel (device, VRAM, CC, features, tier, model table per
  operation) in the settings dialog when a GPU is detected.

https://claude.ai/code/session_01WVDg7amsy1TTtxvpku7bcM
2026-06-13 15:58:13 +00:00

248 lines
8.6 KiB
JavaScript

/**
* API Service for communicating with the FastAPI backend
* Handles SAM selection and AI inpainting requests
*/
class ApiService {
constructor() {
// Backend API base URL
// In unified container: empty string (same origin)
// With separate nginx frontend: '/api' (proxied to backend)
this.baseUrl = window.API_BASE_URL || '';
}
/**
* Call SAM (Segment Anything Model) for smart selection
* @param {string} imageData - Base64 encoded image data
* @param {number} pointX - X coordinate of click point
* @param {number} pointY - Y coordinate of click point
* @returns {Promise<{mask: ImageData, polygon: Array}>}
*/
async smartSelect(imageData, pointX, pointY) {
const response = await fetch(`${this.baseUrl}/tools/smart-select-base64`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
image: imageData,
point_x: pointX,
point_y: pointY,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(error.detail || `SAM request failed: ${response.status}`);
}
return response.json();
}
/**
* Call AI inpainting to edit a selected region
* @param {string} imageData - Base64 encoded image data
* @param {string} maskData - Base64 encoded mask data (white = area to edit)
* @param {string} prompt - Text prompt describing desired edit
* @param {Object} options - Additional options
* @returns {Promise<{result: string}>} - Base64 encoded result image
*/
async inpaint(imageData, maskData, prompt, options = {}) {
const response = await fetch(`${this.baseUrl}/tools/inpaint`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
image: imageData,
mask: maskData,
prompt: prompt,
negative_prompt: options.negativePrompt || '',
strength: options.strength || 0.8,
guidance_scale: options.guidanceScale || 7.5,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(error.detail || `Inpaint request failed: ${response.status}`);
}
return response.json();
}
/**
* Remove background from image using AI (rembg)
* @param {string} imageData - Base64 encoded image data
* @returns {Promise<{result: string, width: number, height: number}>} - Base64 encoded result with transparency
*/
async removeBackground(imageData) {
const response = await fetch(`${this.baseUrl}/tools/remove-background-base64`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
image: imageData,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(error.detail || `Remove background request failed: ${response.status}`);
}
return response.json();
}
/**
* AI erase using LaMa (local, no API key needed)
* @param {string} imageData - Base64 encoded image
* @param {string} maskData - Base64 encoded mask (white = erase)
* @returns {Promise<{result: string, method: string}>}
*/
async erase(imageData, maskData) {
const response = await fetch(`${this.baseUrl}/api/erase`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: imageData, mask: maskData }),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(error.detail || `Erase request failed: ${response.status}`);
}
return response.json();
}
/**
* Text-to-image via remote provider
* @param {string} prompt
* @param {Object} options - width, height, negativePrompt, steps, cfgScale, model
* @returns {Promise<{result: string}>}
*/
async textToImage(prompt, options = {}) {
const response = await fetch(`${this.baseUrl}/api/generate/txt2img`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt,
width: options.width || 1024,
height: options.height || 1024,
negative_prompt: options.negativePrompt || '',
steps: options.steps || 30,
cfg_scale: options.cfgScale || 7.5,
model: options.model || null,
seed: options.seed || 0,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(error.detail || `Text-to-image failed: ${response.status}`);
}
return response.json();
}
/**
* Image-to-image via remote provider
* @param {string} imageData - Base64 encoded image
* @param {string} prompt
* @param {Object} options
* @returns {Promise<{result: string}>}
*/
async imageToImage(imageData, prompt, options = {}) {
const response = await fetch(`${this.baseUrl}/api/generate/img2img`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image: imageData,
prompt,
strength: options.strength || 0.75,
negative_prompt: options.negativePrompt || '',
steps: options.steps || 30,
cfg_scale: options.cfgScale || 7.5,
model: options.model || null,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(error.detail || `Image-to-image failed: ${response.status}`);
}
return response.json();
}
/**
* Inpaint with prompt via remote provider
* @param {string} imageData - Base64
* @param {string} maskData - Base64
* @param {string} prompt
* @param {Object} options
* @returns {Promise<{result: string}>}
*/
async remoteInpaint(imageData, maskData, prompt, options = {}) {
const response = await fetch(`${this.baseUrl}/api/inpaint/remote`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image: imageData,
mask: maskData,
prompt,
negative_prompt: options.negativePrompt || '',
steps: options.steps || 30,
cfg_scale: options.cfgScale || 7.5,
model: options.model || null,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(error.detail || `Remote inpaint failed: ${response.status}`);
}
return response.json();
}
/**
* Fetch backend capabilities (local tools available, remote provider status).
* @returns {Promise<Object>}
*/
async getConfig() {
try {
const response = await fetch(`${this.baseUrl}/api/config`);
if (!response.ok) return null;
return response.json();
} catch {
return null;
}
}
/**
* Fetch GPU status: hardware, feature flags, and selected models per operation.
* Only meaningful when AI_PROVIDER=local_gpu.
* @returns {Promise<Object|null>}
*/
async getGpuStatus() {
try {
const response = await fetch(`${this.baseUrl}/api/gpu/status`);
if (!response.ok) return null;
return response.json();
} catch {
return null;
}
}
/**
* Health check for the backend
* @returns {Promise<boolean>}
*/
async healthCheck() {
try {
const response = await fetch(`${this.baseUrl}/health`);
return response.ok;
} catch {
return false;
}
}
}
// Singleton instance
const apiService = new ApiService();
export default apiService;