1. 引言

在跨语言交流日益频繁的今天,图片中的文字翻译需求越来越常见。本文将带你从零构建一个图片翻译助手,它能够将上传图片中的文字从源语言翻译为目标语言,并保留原始排版。

为什么不用电商平台的通用 API? 传统电商平台提供的图片翻译 API 通常按调用次数收费,单次价格在 0.05~0.2 元/张 不等,且往往需要绑定企业认证、签署合同,个人开发者或小团队很难低成本接入。而本文方案采用 Qwen-MT-Image(约 0.0051 元/张),成本仅为电商 API 的 1/10~1/40,且无需企业资质,注册即可使用,真正实现「低成本、高可用」的图片翻译能力。
技术栈:

  • Streamlit:快速搭建交互式前端
  • LangChain:Agent 框架,编排工具调用
  • DeepSeek Chat:作为 Agent 的 LLM 核心
  • Qwen-MT-Image:阿里云通义千问图片翻译模型
  • ImgBB:免费图床,用于获取图片公网 URL

最终效果:用户上传图片或输入图片 URL,选择源语言和目标语言,点击翻译即可获得翻译后的图片。

2. 环境准备与依赖安装

首先创建项目目录并安装依赖:

mkdir image-translator
cd image-translator
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

创建 requirements.txt

streamlit>=1.28.0
langchain>=0.3.0
langchain-deepseek>=0.1.0
langchain-classic>=0.3.0
python-dotenv>=1.0.0
Pillow>=10.0.0
requests>=2.31.0

安装依赖:

pip install -r requirements.txt

3. 环境变量配置

在项目根目录创建 .env 文件,填入以下密钥:

DASHSCOPE_API_KEY=你的阿里云通义千问API密钥
DEEPSEEK_API_KEY=你的DeepSeek API密钥
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
IMGBB_API_KEY=你的ImgBB API密钥

获取方式

4. 核心代码实现

4.1 导入依赖与加载环境变量

import os
import time
import json
import re
import requests
import streamlit as st
from dotenv import load_dotenv
from PIL import Image
from pydantic import BaseModel, Field
from typing import Optional
from langchain.tools import tool
from langchain_deepseek import ChatDeepSeek
from langchain_classic.agents import create_tool_calling_agent, AgentExecutor
from langchain_classic.prompts import ChatPromptTemplate, MessagesPlaceholder
import langchain
import langchain_core

# ---------- 打印版本(用于调试) ----------
print("langchain版本:", langchain.__version__)
print("langchain-core版本:", langchain_core.__version__)

# ---------- 加载环境变量 ----------
load_dotenv()

DASHSCOPE_API_KEY = os.getenv("DASHSCOPE_API_KEY")
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1")
IMGBB_API_KEY = os.getenv("IMGBB_API_KEY")

4.2 配置 API 密钥

DASHSCOPE_API_KEY = os.getenv("DASHSCOPE_API_KEY")
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1")
IMGBB_API_KEY = os.getenv("IMGBB_API_KEY")

4.3 实现 ImgBB 图床上传工具

当用户上传本地图片时,需要先将其上传到公网图床,才能传给 Qwen-MT-Image API:

def upload_to_imgbb(image_bytes, api_key: str) -> str:
    """
    将图片二进制数据上传到 ImgBB,返回公网 URL
    """
    if not api_key:
        raise ValueError("未设置 IMGBB_API_KEY,请在 .env 文件中添加或前往 https://api.imgbb.com/ 注册获取")
    url = "https://api.imgbb.com/1/upload"
    payload = {"key": api_key}
    files = {"image": image_bytes}
    resp = requests.post(url, data=payload, files=files, timeout=30)
    resp.raise_for_status()
    data = resp.json()
    if data.get("success"):
        return data["data"]["url"]
    else:
        raise Exception(f"ImgBB 上传失败: {data}")

4.4 定义 LangChain Tool:图片翻译

这是核心工具函数,调用阿里云 Qwen-MT-Image 模型进行图片翻译:

# ---------- 1. 定义结构化输出模型 ----------
class TranslationResult(BaseModel):
    status: str = Field(..., description="success 或 error")
    image_url: Optional[str] = Field(None, description="翻译后的图片 URL(成功时必填)")
    error_message: Optional[str] = Field(None, description="错误详情(失败时必填)")

# ---------- 2. 工具函数:上传图片到 ImgBB ----------
def upload_to_imgbb(image_bytes, api_key: str) -> str:
    """
    将图片二进制数据上传到 ImgBB,返回公网 URL
    """
    if not api_key:
        raise ValueError("未设置 IMGBB_API_KEY,请在 .env 文件中添加")
    url = "https://api.imgbb.com/1/upload"
    payload = {"key": api_key}
    files = {"image": image_bytes}
    resp = requests.post(url, data=payload, files=files, timeout=30)
    resp.raise_for_status()
    data = resp.json()
    if data.get("success"):
        return data["data"]["url"]
    else:
        raise Exception(f"ImgBB 上传失败: {data}")

# ---------- 3. LangChain Tool(带重试和指数退避) ----------
@tool
def translate_image(image_url: str, source_lang: str = "zh", target_lang: str = "ru") -> str:
    """
    将图片中的文字从源语言翻译为目标语言,保留原始排版。
    返回 JSON 字符串,格式为 {"status": "success", "image_url": "..."}
    或 {"status": "error", "error_message": "..."}
    """
    if not image_url:
        return json.dumps(TranslationResult(status="error", error_message="缺少 image_url 参数").dict())

    create_url = "https://llm-2p1o6fdpikmzdwi8.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/image2image/image-synthesis"
    headers = {
        "Authorization": f"Bearer {DASHSCOPE_API_KEY}",
        "Content-Type": "application/json",
        "X-DashScope-Async": "enable"
    }
    payload = {
        "model": "qwen-mt-image",
        "input": {
            "image_url": image_url,
            "source_lang": source_lang,
            "target_lang": target_lang
        }
    }

    try:
        # 步骤1:创建任务
        response = requests.post(create_url, headers=headers, json=payload, timeout=30)
        response.raise_for_status()
        data = response.json()
        task_id = data.get("output", {}).get("task_id")
        if not task_id:
            return json.dumps(TranslationResult(status="error", error_message=f"创建任务失败: {data}").dict())

        # 步骤2:轮询查询任务状态(带指数退避重试)
        query_url = f"https://llm-2p1o6fdpikmzdwi8.cn-beijing.maas.aliyuncs.com/api/v1/tasks/{task_id}"
        max_attempts = 15  # 最多尝试15次
        backoff = 2        # 初始等待秒数
        for attempt in range(max_attempts):
            time.sleep(backoff)  # 首次等待2秒
            qr = requests.get(query_url, headers={"Authorization": f"Bearer {DASHSCOPE_API_KEY}"})
            if qr.status_code != 200:
                try:
                    err = qr.json().get("message", qr.text)
                except:
                    err = qr.text
                return json.dumps(TranslationResult(status="error", error_message=f"查询任务失败: {err}").dict())
            result = qr.json()
            status = result.get("output", {}).get("task_status")
            if status == "SUCCEEDED":
                translated_url = result.get("output", {}).get("image_url")
                if translated_url:
                    return json.dumps(TranslationResult(status="success", image_url=translated_url).dict())
                else:
                    return json.dumps(
                        TranslationResult(status="error", error_message="任务完成但未返回图片 URL").dict())
            elif status == "FAILED":
                msg = result.get("output", {}).get("message", "无详细信息")
                return json.dumps(TranslationResult(status="error", error_message=f"任务失败: {msg}").dict())
            # 否则继续等待(PENDING 或 RUNNING)
            # 指数退避:下次等待时间加倍,但不超过10秒
            backoff = min(backoff * 1.5, 10)

        # 超时未完成
        return json.dumps(TranslationResult(status="error", error_message="翻译任务超时,请稍后重试").dict())
    except requests.exceptions.RequestException as e:
        return json.dumps(TranslationResult(status="error", error_message=f"API 调用失败: {str(e)}").dict())
    except Exception as e:
        return json.dumps(TranslationResult(status="error", error_message=f"发生未知错误: {str(e)}").dict())

4.5 创建 Agent

使用 LangChain 的 create_tool_calling_agent 构建 Agent,将翻译工具注册进去:

def create_agent():
    llm = ChatDeepSeek(
        model="deepseek-chat",
        api_key=DEEPSEEK_API_KEY,
        base_url=DEEPSEEK_BASE_URL,
        temperature=0
    )
    tools = [translate_image]
    prompt = ChatPromptTemplate.from_messages([
        ("system", """你是一个图片翻译助手。用户会提供图片 URL 和翻译需求。
        请调用 translate_image 工具完成翻译。
        支持的语言代码:
        - 中文: zh, 英文: en, 日文: ja, 韩语: ko
        - 西班牙语: es, 法语: fr, 葡萄牙语: pt, 阿拉伯语: ar
        - 俄语: ru
        注意:Qwen-MT-Image 只支持中文或英文与其他语种之间的互译,
        不支持两个非中/英语种之间直接翻译。
        """),
        ("user", "{input}"),
        MessagesPlaceholder(variable_name="agent_scratchpad")
    ])
    agent = create_tool_calling_agent(llm, tools, prompt)
    executor = AgentExecutor(
        agent=agent,
        tools=tools,
        verbose=True,
        handle_parsing_errors=True
    )
    return executor

4.6 Streamlit 前端界面

最后是用户交互界面,包含侧边栏设置、图片上传/URL输入、翻译按钮和结果展示:

# ---------- 5. 缓存包装器(基于输入参数缓存翻译结果) ----------
@st.cache_data(ttl=3600)  # 缓存1小时
def translate_with_cache(image_url: str, source_lang: str, target_lang: str) -> str:
    """
    调用 Agent 并返回原始输出字符串(JSON)。
    缓存键为 (image_url, source_lang, target_lang)
    """
    executor = create_agent()
    result = executor.invoke({
        "input": f"请将图片 {image_url} 中的文字从 {source_lang} 翻译为 {target_lang}"
    })
    return result.get("output", "")

# ---------- 6. Streamlit 前端 ----------
st.set_page_config(page_title="图片翻译助手", page_icon="🌎", layout="wide")
st.title("🌎 宽仔图片翻译")
st.markdown("上传图片或输入图片 URL,将图片中的文字翻译成目标语言")

# 侧边栏
with st.sidebar:
    st.header("⚙️ 翻译设置")
    source_lang = st.selectbox(
        "源语言",
        options=["zh", "en", "ja", "ko", "es", "fr", "pt", "ar"],
        format_func=lambda x: {
            "zh": "中文", "en": "英文", "ja": "日文", "ko": "韩语",
            "es": "西班牙语", "fr": "法语", "pt": "葡萄牙语", "ar": "阿拉伯语"
        }.get(x, x),
        index=0
    )
    target_lang = st.selectbox(
        "目标语言",
        options=["ru", "en", "zh", "ja", "ko", "es", "fr", "pt", "ar"],
        format_func=lambda x: {
            "ru": "俄文", "zh": "中文", "en": "英文", "ja": "日文",
            "ko": "韩语", "es": "西班牙语", "fr": "法语",
            "pt": "葡萄牙语", "ar": "阿拉伯语"
        }.get(x, x),
        index=0
    )
    st.info("💡 提示:Qwen-MT-Image 支持中文或英文与其他语种互译")

# 主区域
input_method = st.radio("选择输入方式", ["上传图片", "图片 URL"], horizontal=True)
image_url = None

if input_method == "上传图片":
    uploaded_file = st.file_uploader(
        "点击上传图片",
        type=["jpg", "jpeg", "png", "bmp", "tiff", "webp"],
        help="支持 JPG、PNG、BMP、WEBP 等格式"
    )
    if uploaded_file is not None:
        # 显示原图
        image = Image.open(uploaded_file)
        st.image(image, caption="原图", use_container_width=True)
        # 检查 ImgBB API Key
        if not IMGBB_API_KEY:
            st.error("❌ 未设置 ImgBB API Key,请在 .env 文件中添加 IMGBB_API_KEY 或前往 https://api.imgbb.com/ 注册获取")
            st.stop()
        # 自动上传到 ImgBB
        with st.spinner("正在上传图片到 ImgBB 图床..."):
            try:
                img_bytes = uploaded_file.getvalue()
                image_url = upload_to_imgbb(img_bytes, IMGBB_API_KEY)
                st.success("✅ 图片已上传,公网 URL 获取成功")
                st.caption(f"URL: {image_url}")
            except Exception as e:
                st.error(f"❌ 上传失败: {str(e)}")
                st.stop()
else:  # 图片 URL 方式
    image_url = st.text_input(
        "输入图片公网 URL",
        placeholder="https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/6962921671/p1006389.webp",
        help="图片 URL 不能包含中文字符"
    )
    if image_url:
        try:
            st.image(image_url, caption="原图", use_container_width=True)
        except Exception:
            st.error("无法加载图片,请检查 URL 是否有效")

# 翻译按钮
if st.button("🚀 开始翻译", type="primary", disabled=not image_url):
    if not DASHSCOPE_API_KEY:
        st.error("❌ 请先设置 DASHSCOPE_API_KEY 环境变量")
        st.stop()
    if not DEEPSEEK_API_KEY:
        st.error("❌ 请先设置 DEEPSEEK_API_KEY 环境变量")
        st.stop()

    with st.spinner("🔄 正在翻译图片,请稍候..."):
        try:
            # 调用带缓存的翻译函数
            output = translate_with_cache(image_url, source_lang, target_lang)
            # 解析 JSON
            try:
                data = json.loads(output)
                if data.get("status") == "success":
                    translated_url = data.get("image_url")
                    if translated_url:
                        st.success("✅ 翻译完成!")
                        col1, col2 = st.columns(2)
                        with col1:
                            st.subheader("📷 原图")
                            st.image(image_url, use_container_width=True)
                        with col2:
                            st.subheader("🇷🇺 翻译后")
                            st.image(translated_url, use_container_width=True)
                        st.caption(f"翻译结果 URL:{translated_url}")
                    else:
                        st.error("❌ 翻译成功但未返回图片 URL,请检查 API 响应")
                else:
                    error_msg = data.get("error_message", "未知错误")
                    st.error(f"❌ 翻译失败:{error_msg}")
            except json.JSONDecodeError:
                st.error(f"❌ Agent 返回了非 JSON 格式的内容,请检查 API 配置。原始输出:\n```\n{output}\n```")
                with st.expander("查看完整输出"):
                    st.text(output)
        except Exception as e:
            st.error(f"❌ 发生错误:{str(e)}")

# 使用说明
with st.expander("📖 使用说明"):
    st.markdown("""
    ### 支持的语言
    - **源语言**:中文(zh)、英文(en)、日文(ja)、韩语(ko)、西班牙语(es)、法语(fr)、葡萄牙语(pt)、阿拉伯语(ar)
    - **目标语言**:同上 + 俄文(ru)

    ### 上传方式
    - **上传图片**:自动通过 ImgBB 图床获取公网 URL(需要 ImgBB API Key)
    - **图片 URL**:直接输入任何公网可访问的图片链接

    ### 如何获取 ImgBB API Key?
    1. 访问 https://api.imgbb.com/
    2. 注册账号(免费)
    3. 在 Dashboard 中找到 API Key
    4. 将其添加到项目根目录的 `.env` 文件中:`IMGBB_API_KEY=你的key`

    ### 注意事项
    1. Qwen-MT-Image 只支持**中文或英文**与其他语种之间的互译
    2. 图片尺寸:15-8192 像素,宽高比 1:10 至 10:1
    3. 图片大小不超过 100MB
    4. 翻译结果会缓存 1 小时,相同图片重复翻译不会重复计费

    ### 费用说明
    - **Qwen-MT-Image**:约 **0.0051 元/张**
    - **ImgBB**:免费(有每日上传限制,个人使用足够)
    - **DeepSeek Agent**:极少量文本 Token,成本极低
    """)

}")


## 5. 运行应用

在项目根目录执行以下命令启动 Streamlit 应用:

```bash
python -m streamlit run app.py

浏览器会自动打开 http://localhost:8501,你将看到完整的图片翻译界面。

6. 使用说明

支持的语言

语言 代码
中文 zh
英文 en
日文 ja
韩语 ko
西班牙语 es
法语 fr
葡萄牙语 pt
阿拉伯语 ar
俄语 ru

注意:Qwen-MT-Image 只支持中文或英文与其他语种之间的互译,不支持两个非中/英语种之间直接翻译。

上传方式

  1. 上传图片:自动通过 ImgBB 图床获取公网 URL(需要 ImgBB API Key)
  2. 图片 URL:直接输入任何公网可访

费用说明

  • Qwen-MT-Image:约 0.0051 元/张(仅为电商通用图片翻译 API 的 1/10~1/40)
  • ImgBB:免费(有每日上传限制,个人使用足够)
  • DeepSeek Agent:极少文本 Token,成本极低
  • 电商 API 对比:主流电商平台图片翻译 API 单价约 0.05~0.2 元/张,且通常要求企业认证、预充值、签署服务协议,个人开发者门槛高。本文方案无需企业资质,注册即用,成本优势显著。0~1/40)
  • ImgBB:免费(有每日上传限制,个人使用足够)
  • DeepSeek Agent:极少文本 Token,成本极低
  • 电商 API 对比:主流电商平台图片翻译 API 单价约 0.05~0.2 元/张,且通常要求企业认证、预充值、签署服务协议,个人开发者门槛高。本文方案无需企业资质,注册即用,成本优势显著。n,成本极低

7. 总结

本文实现了一个完整的图片翻译助手,核心流程如下:

  1. 用户上传图片或输入图片 URL
  2. 如果是上传图片,先通过 ImgBB 获取公网 URL
  3. LangChain Agent 调用 Qwen-MT-Image API 进行图片翻译
  4. 展示原图与翻译结果对比

你可以在此基础上扩展更多功能,例如:

  • 支持批量翻译多张图片
  • 添加翻译历史记录
  • 集成更多图片翻译模型
  • 优化 UI 交互体验

完整的源代码已在上文展示,效果如下:
在这里插入图片描述

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐