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": "工具已显示", "Tool shown successfully": "工具已显示",
"Default tools cannot be edited": "默认工具无法编辑", "Default tools cannot be edited": "默认工具无法编辑",
"Hidden Tools": "已隐藏的工具", "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"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { NButton, useMessage } from 'naive-ui' import { NButton, useMessage } from 'naive-ui'
import axios from 'axios' import axios from 'axios'
import { t } from '@/shared/i18n'
import { get_session_api_headers } from '@/shared/api/auth' import { get_session_api_headers } from '@/shared/api/auth'
import { useAuthStore } from '@/shared/stores/auth'
const { t } = useI18n()
const message = useMessage() const message = useMessage()
const authStore = useAuthStore()
// //
const fileInputRef = ref<HTMLInputElement | null>(null) 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 () => { 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) { if (!uploadedImage.value) {
message.warning(t('Please upload an image first')) message.warning(t('Please upload an image first'))
return return
@ -294,43 +258,82 @@ const handleRemoveBackground = async () => {
resultImage.value = '' resultImage.value = ''
try { try {
// URL // base64
const imageUrl = await uploadImageToServer(uploadedImage.value) const reader = new FileReader()
if (!imageUrl) { const base64Promise = new Promise<string>((resolve, reject) => {
processing.value = false reader.onload = (e) => {
return 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( const response = await axios.post(
'/jingrow.tools.tools.remove_background', '/jingrow.tools.tools.remove_background',
{ {
image_urls: [imageUrl] image_data: base64Data
}, },
{ {
headers: get_session_api_headers(), headers: get_session_api_headers(),
withCredentials: true, withCredentials: true, // session cookie
timeout: 180000 // 3 timeout: 180000 // 3
} }
) )
if (response.data?.success && response.data?.data?.length > 0) { // whitelist : {success: true, data: function_result}
const result = response.data.data[0] // function_result : {success: true/false, data: [...], ...}
if (result.success && result.image_content) { if (response.data?.success && response.data?.data) {
resultImage.value = result.image_content const result = response.data.data
message.success(t('Background removed successfully!'))
// 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 { } else {
//
message.error(result.error || t('Failed to remove background')) message.error(result.error || t('Failed to remove background'))
} }
} else { } 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) { } catch (error: any) {
console.error('Remove background error:', error) let errorMessage = t('Failed to remove background')
const errorMessage = error.response?.data?.error ||
error.response?.data?.detail || if (error.response) {
error.message || //
t('Failed to remove background') 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) message.error(errorMessage)
} finally { } finally {
processing.value = false processing.value = false
@ -342,11 +345,12 @@ const handleDownload = () => {
if (!resultImage.value) return if (!resultImage.value) return
try { try {
// base64blob // base64
const base64Data = resultImage.value.includes(',') const base64Data = resultImage.value.includes(',')
? resultImage.value.split(',')[1] ? resultImage.value.split(',')[1]
: resultImage.value : resultImage.value
// base64
const byteCharacters = atob(base64Data) const byteCharacters = atob(base64Data)
const byteNumbers = new Array(byteCharacters.length) const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) { for (let i = 0; i < byteCharacters.length; i++) {
@ -355,19 +359,25 @@ const handleDownload = () => {
const byteArray = new Uint8Array(byteNumbers) const byteArray = new Uint8Array(byteNumbers)
const blob = new Blob([byteArray], { type: 'image/png' }) const blob = new Blob([byteArray], { type: 'image/png' })
// // 使 blob URL
// HTTP
// HTTPS
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
const link = document.createElement('a') const link = document.createElement('a')
link.href = url link.href = url
link.download = `removed-background-${Date.now()}.png` link.download = `removed-background-${Date.now()}.png`
link.style.display = 'none'
document.body.appendChild(link) document.body.appendChild(link)
link.click() link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url) //
setTimeout(() => {
document.body.removeChild(link)
URL.revokeObjectURL(url)
}, 100)
message.success(t('Download started')) message.success(t('Download started'))
} catch (error: any) { } catch (error: any) {
console.error('Download error:', error)
message.error(t('Failed to download image')) message.error(t('Failed to download image'))
} }
} }

View File

@ -7,9 +7,8 @@ Jingrow Tools API
""" """
import base64 import base64
import json
import requests import requests
from typing import Dict, Any, List, Union from typing import Dict, Any, Union
import logging import logging
import jingrow import jingrow
@ -19,139 +18,117 @@ logger = logging.getLogger(__name__)
@jingrow.whitelist() @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 实现图片背景移除 调用 Jingrow Cloud API 实现图片背景移除
Args: Args:
image_urls (str | List[str]): 图片URL单个URL字符串或URL列表 image_data: Base64编码的图片数据data:image/开头
Returns: Returns:
dict: 处理结果 dict: 处理结果
{
'success': bool,
'data': List[dict], # 处理结果列表(成功时)
'error': str, # 错误信息(失败时)
'total_processed': int, # 处理总数
'total_success': int # 成功数量
}
""" """
try: try:
# 获取API URL和认证头 # 获取认证头
api_url = f"{get_jingrow_cloud_api_url()}/rmbg/batch"
headers = get_jingrow_cloud_api_headers() headers = get_jingrow_cloud_api_headers()
if not headers or not headers.get('Authorization'): if not headers or not headers.get('Authorization'):
error_message = "API密钥未设置请在设置中配置 Jingrow Cloud Api Key 和 Jingrow Cloud Api Secret"
logger.error(error_message)
return { return {
"success": False, "success": False,
"error": error_message "error": "API密钥未设置请在设置中配置 Jingrow Cloud Api Key 和 Jingrow Cloud Api Secret"
} }
# 处理单个URL或URL列表 # 处理单个数据
if isinstance(image_urls, str): if isinstance(image_data, list):
image_urls = [image_urls] image_data = image_data[0] if image_data else None
if not image_urls: if not image_data:
return { return {
"success": False, "success": False,
"error": "未提供图片URL" "error": "未提供图片数据"
} }
# 准备请求数据 # 提取base64部分
request_data = {"urls": image_urls} 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( response = requests.post(
api_url, api_url,
json=request_data, files=files,
headers=headers, headers=upload_headers,
stream=True,
timeout=180 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: try:
error_data = response.json() error_data = response.json()
error_message = error_data.get("message") or error_data.get("error") or f"API请求失败 (HTTP {response.status_code})" error_message = error_data.get("message") or error_data.get("error") or f"API请求失败 (HTTP {response.status_code})"
except: except:
error_message = f"API请求失败 (HTTP {response.status_code})" 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 { return {
"success": False, "success": False,
"error": error_message "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: except Exception as e:
error_message = f"调用图片去背景API异常{str(e)}" logger.error(f"调用图片去背景API异常{str(e)}", exc_info=True)
logger.error(error_message, exc_info=True)
return { return {
"success": False, "success": False,
"error": error_message "error": f"调用图片去背景API异常{str(e)}"
} }