add image background removal tool

This commit is contained in:
jingrow 2025-11-19 04:26:42 +08:00
parent bf9d52a2db
commit 77982b66ff
3 changed files with 163 additions and 169 deletions

View File

@ -1117,5 +1117,12 @@
"Tool shown successfully": "工具已显示",
"Default tools cannot be edited": "默认工具无法编辑",
"Hidden Tools": "已隐藏的工具",
"Click to show hidden tools": "点击显示隐藏的工具"
"Click to show hidden tools": "点击显示隐藏的工具",
"API endpoint not found. Please check if the backend service is running.": "API端点未找到请检查后端服务是否运行",
"Authentication failed. Please login again.": "认证失败,请重新登录",
"Network error. Please check your connection.": "网络错误,请检查您的连接",
"No image data returned": "未返回图片数据",
"Please login first to use this feature": "请先登录以使用此功能",
"Session expired. Please login again.": "会话已过期,请重新登录",
"Session expired. Please login again to use this feature.": "会话已过期,请重新登录以使用此功能"
}

View File

@ -140,13 +140,14 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { NButton, useMessage } from 'naive-ui'
import axios from 'axios'
import { t } from '@/shared/i18n'
import { get_session_api_headers } from '@/shared/api/auth'
import { useAuthStore } from '@/shared/stores/auth'
const { t } = useI18n()
const message = useMessage()
const authStore = useAuthStore()
//
const fileInputRef = ref<HTMLInputElement | null>(null)
@ -237,54 +238,17 @@ const resetUpload = () => {
}
}
// URL
const uploadImageToServer = async (file: File): Promise<string | null> => {
try {
// base64使download_image_to_local API
const reader = new FileReader()
const base64Promise = new Promise<string>((resolve, reject) => {
reader.onload = (e) => {
const result = e.target?.result as string
resolve(result)
}
reader.onerror = reject
})
reader.readAsDataURL(file)
const dataUrl = await base64Promise
// 使download_image_to_local API
const response = await axios.post(
'/jingrow.utils.fs.download_image_to_local',
{
image_url: dataUrl,
filename: file.name
},
{
headers: get_session_api_headers(),
withCredentials: true,
timeout: 30000
}
)
if (response.data?.data?.success && response.data?.data?.local_path) {
// URL
const fileUrl = response.data.data.local_path
if (fileUrl.startsWith('/')) {
return `${window.location.origin}${fileUrl}`
}
return fileUrl
}
return null
} catch (error: any) {
console.error('Upload error:', error)
message.error(t('Failed to upload image: ') + (error.response?.data?.detail || error.message))
return null
}
}
//
const handleRemoveBackground = async () => {
//
if (!authStore.isLoggedIn) {
message.error(t('Please login first to use this feature'))
return
}
// cookie API
// whitelist cookie 401
if (!uploadedImage.value) {
message.warning(t('Please upload an image first'))
return
@ -294,43 +258,82 @@ const handleRemoveBackground = async () => {
resultImage.value = ''
try {
// URL
const imageUrl = await uploadImageToServer(uploadedImage.value)
if (!imageUrl) {
processing.value = false
return
}
// base64
const reader = new FileReader()
const base64Promise = new Promise<string>((resolve, reject) => {
reader.onload = (e) => {
const result = e.target?.result as string
resolve(result)
}
reader.onerror = reject
})
reader.readAsDataURL(uploadedImage.value)
const base64Data = await base64Promise
// API
// API使 withCredentials session cookie
const response = await axios.post(
'/jingrow.tools.tools.remove_background',
{
image_urls: [imageUrl]
image_data: base64Data
},
{
headers: get_session_api_headers(),
withCredentials: true,
withCredentials: true, // session cookie
timeout: 180000 // 3
}
)
if (response.data?.success && response.data?.data?.length > 0) {
const result = response.data.data[0]
if (result.success && result.image_content) {
resultImage.value = result.image_content
message.success(t('Background removed successfully!'))
// whitelist : {success: true, data: function_result}
// function_result : {success: true/false, data: [...], ...}
if (response.data?.success && response.data?.data) {
const result = response.data.data
// success
if (result.success) {
// data
if (result.data && Array.isArray(result.data) && result.data.length > 0) {
const firstResult = result.data[0]
if (firstResult.success && firstResult.image_content) {
resultImage.value = firstResult.image_content
message.success(t('Background removed successfully!'))
} else {
message.error(firstResult.error || t('Failed to remove background'))
}
} else {
message.error(t('No image data returned'))
}
} else {
//
message.error(result.error || t('Failed to remove background'))
}
} else {
message.error(response.data?.error || t('Failed to remove background'))
// whitelist
message.error(response.data?.error || response.data?.message || t('Failed to remove background'))
}
} catch (error: any) {
console.error('Remove background error:', error)
const errorMessage = error.response?.data?.error ||
error.response?.data?.detail ||
error.message ||
t('Failed to remove background')
let errorMessage = t('Failed to remove background')
if (error.response) {
//
if (error.response.data) {
errorMessage = error.response.data.error ||
error.response.data.detail ||
error.response.data.message ||
errorMessage
}
if (error.response.status === 404) {
errorMessage = t('API endpoint not found. Please check if the backend service is running.')
} else if (error.response.status === 401 || error.response.status === 403) {
errorMessage = t('Authentication failed. Please login again.')
}
} else if (error.request) {
//
errorMessage = t('Network error. Please check your connection.')
} else {
//
errorMessage = error.message || errorMessage
}
message.error(errorMessage)
} finally {
processing.value = false
@ -342,11 +345,12 @@ const handleDownload = () => {
if (!resultImage.value) return
try {
// base64blob
// base64
const base64Data = resultImage.value.includes(',')
? resultImage.value.split(',')[1]
: resultImage.value
// base64
const byteCharacters = atob(base64Data)
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
@ -355,19 +359,25 @@ const handleDownload = () => {
const byteArray = new Uint8Array(byteNumbers)
const blob = new Blob([byteArray], { type: 'image/png' })
//
// 使 blob URL
// HTTP
// HTTPS
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `removed-background-${Date.now()}.png`
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
//
setTimeout(() => {
document.body.removeChild(link)
URL.revokeObjectURL(url)
}, 100)
message.success(t('Download started'))
} catch (error: any) {
console.error('Download error:', error)
message.error(t('Failed to download image'))
}
}

View File

@ -7,9 +7,8 @@ Jingrow Tools API
"""
import base64
import json
import requests
from typing import Dict, Any, List, Union
from typing import Dict, Any, Union
import logging
import jingrow
@ -19,139 +18,117 @@ logger = logging.getLogger(__name__)
@jingrow.whitelist()
def remove_background(image_urls: Union[str, List[str]]) -> Dict[str, Any]:
def remove_background(image_data: Union[str, list]) -> Dict[str, Any]:
"""
图片去背景工具
调用 Jingrow Cloud API 实现图片背景移除
Args:
image_urls (str | List[str]): 图片URL单个URL字符串或URL列表
image_data: Base64编码的图片数据data:image/开头
Returns:
dict: 处理结果
{
'success': bool,
'data': List[dict], # 处理结果列表(成功时)
'error': str, # 错误信息(失败时)
'total_processed': int, # 处理总数
'total_success': int # 成功数量
}
"""
try:
# 获取API URL和认证头
api_url = f"{get_jingrow_cloud_api_url()}/rmbg/batch"
# 获取认证头
headers = get_jingrow_cloud_api_headers()
if not headers or not headers.get('Authorization'):
error_message = "API密钥未设置请在设置中配置 Jingrow Cloud Api Key 和 Jingrow Cloud Api Secret"
logger.error(error_message)
return {
"success": False,
"error": error_message
"error": "API密钥未设置请在设置中配置 Jingrow Cloud Api Key 和 Jingrow Cloud Api Secret"
}
# 处理单个URL或URL列表
if isinstance(image_urls, str):
image_urls = [image_urls]
# 处理单个数据
if isinstance(image_data, list):
image_data = image_data[0] if image_data else None
if not image_urls:
if not image_data:
return {
"success": False,
"error": "未提供图片URL"
"error": "未提供图片数据"
}
# 准备请求数据
request_data = {"urls": image_urls}
# 提取base64部分
if ',' in image_data:
header, base64_content = image_data.split(',', 1)
if 'png' in header:
ext = 'png'
content_type = 'image/png'
elif 'jpeg' in header or 'jpg' in header:
ext = 'jpg'
content_type = 'image/jpeg'
elif 'webp' in header:
ext = 'webp'
content_type = 'image/webp'
else:
ext = 'png'
content_type = 'image/png'
else:
base64_content = image_data
ext = 'png'
content_type = 'image/png'
# 调用API并获取流式响应
# 解码base64数据
image_bytes = base64.b64decode(base64_content)
# 调用文件上传端点
api_url = f"{get_jingrow_cloud_api_url()}/rmbg/file"
# 移除 Content-Type让 requests 自动设置 multipart/form-data
upload_headers = {k: v for k, v in headers.items() if k.lower() != 'content-type'}
files = {
'file': (f'image.{ext}', image_bytes, content_type)
}
# 转发请求
response = requests.post(
api_url,
json=request_data,
headers=headers,
stream=True,
files=files,
headers=upload_headers,
timeout=180
)
if response.status_code != 200:
if response.status_code == 200:
result_data = response.json()
# 处理返回的图片内容
if 'image_content' in result_data:
image_content = result_data.get('image_content', '')
# 如果包含 data:image 前缀提取base64部分
if ',' in image_content:
image_content = image_content.split(',')[1]
return {
"success": True,
"data": [{
"success": True,
"image_content": image_content,
"index": 1,
"total": 1
}],
"total_processed": 1,
"total_success": 1
}
else:
return {
"success": False,
"error": result_data.get('error', '未知错误')
}
else:
try:
error_data = response.json()
error_message = error_data.get("message") or error_data.get("error") or f"API请求失败 (HTTP {response.status_code})"
except:
error_message = f"API请求失败 (HTTP {response.status_code})"
if response.status_code in [401, 403]:
error_message = "API认证失败请检查认证信息"
logger.error(f"图片去背景API调用失败: {error_message}")
return {
"success": False,
"error": error_message
}
# 处理流式响应
results = []
total_processed = 0
total_success = 0
for line in response.iter_lines():
if line:
try:
result = json.loads(line)
total_processed += 1
if result.get('status') == 'success' and 'image_content' in result:
# 获取图片内容base64编码
image_content = result.get('image_content', '')
# 如果包含 data:image 前缀提取base64部分
if ',' in image_content:
image_content = image_content.split(',')[1]
results.append({
"success": True,
"original_url": result.get('original_url', ''),
"image_content": image_content, # Base64编码的图片数据
"index": result.get('index', 0),
"total": result.get('total', 1)
})
total_success += 1
else:
error_message = result.get('message', '未知错误')
results.append({
"success": False,
"error": error_message,
"original_url": result.get('original_url', ''),
"index": result.get('index', 0),
"total": result.get('total', 1)
})
except Exception as e:
logger.warning(f"解析流式响应行失败: {e}")
continue
if total_processed == 0:
return {
"success": False,
"error": "未能获取到任何响应数据"
}
return {
"success": True,
"data": results,
"total_processed": total_processed,
"total_success": total_success
}
except requests.exceptions.Timeout:
error_message = "请求超时,请稍后重试"
logger.error(f"图片去背景API调用超时: {error_message}")
return {
"success": False,
"error": error_message
}
except Exception as e:
error_message = f"调用图片去背景API异常{str(e)}"
logger.error(error_message, exc_info=True)
logger.error(f"调用图片去背景API异常{str(e)}", exc_info=True)
return {
"success": False,
"error": error_message
"error": f"调用图片去背景API异常{str(e)}"
}