Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,3 +408,7 @@ Thank you, that's enough.

[^token]: https://platform.openai.com/account/api-keys
[^black]: https://github.com/psf/black


# 我的习惯使用命令
python make_book.py --book_name 'test_books/The Theory of Poker -- David Sklansky.epub' -m glm-4-flash --glm_key 'a5f10c98eddf47b38baf055963917ba8.ZP7xvGivBXb8GDUr' --use_context --context_paragraph_limit 5
4 changes: 4 additions & 0 deletions advance_prompt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"system": "我希望你以一个专业翻译团队的身份,协助完成将文章翻译成中文的任务。",
"user": "对于每个翻译任务,我将扮演两个专家角色,分别负责翻译与校对工作:翻译专家:具有20年翻译经验,精通中英双语,并拥有丰富的跨学科知识。此阶段的目标是提供一份既忠实于原文,又在中文中读起来流畅自然的初稿。在翻译时,特别注重保持原文的风格和语调。资深校对编辑:拥有20年专业编辑经验,中文系毕业,对中文语法、用词有精准把握。在此阶段,您需要对翻译稿进行深度校对,包括语法、用词、风格的校正,确保翻译的准确性和易读性,进而输出第二版翻译稿。注意,请只输出译文。{language} 待翻译的内容如下:{text}。"
}
14 changes: 14 additions & 0 deletions book_maker/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,14 @@ def main():
help="You can get Qwen Key from https://bailian.console.aliyun.com/?tab=model#/api-key",
)

# for Zhipu GLM
parser.add_argument(
"--glm_key",
dest="glm_key",
type=str,
help="You can get GLM Key from https://open.bigmodel.cn/usercenter/apikeys",
)

parser.add_argument(
"--test",
dest="test",
Expand Down Expand Up @@ -476,6 +484,10 @@ def main():
API_KEY = options.xai_key or env.get("BBM_XAI_API_KEY")
elif options.model.startswith("qwen-"):
API_KEY = options.qwen_key or env.get("BBM_QWEN_API_KEY")
elif options.model.startswith("glm") or options.model == "glm":
API_KEY = options.glm_key or env.get("BBM_GLM_API_KEY")
if not API_KEY:
raise Exception("Please provide GLM API key via --glm_key or BBM_GLM_API_KEY environment variable")
else:
API_KEY = ""

Expand Down Expand Up @@ -599,6 +611,8 @@ def main():
e.translate_model.set_claude_model(options.model)
if options.model.startswith("qwen-"):
e.translate_model.set_qwen_model(options.model)
if options.model.startswith("glm") or options.model == "glm":
e.translate_model.set_glm_model(options.model)
if options.block_size > 0:
e.block_size = options.block_size
if options.batch_flag:
Expand Down
8 changes: 5 additions & 3 deletions book_maker/loader/epub_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,12 @@ def _process_paragraph(self, p, new_p, index, p_to_save_len, thread_safe=False):
t_text = self.translate_model.batch_translate(index)
else:
t_text = self.translate_model.translate(new_p.text)
if t_text is None:
raise RuntimeError(
"`t_text` is None: your translation model is not working as expected. Please check your translation model configuration."
if t_text is None or t_text == "":
# Translation failed or was filtered - use original text
print(
f"[yellow]⚠ Translation returned None/empty - using original text[/yellow]"
)
t_text = new_p.text
if type(p) is NavigableString:
new_p = t_text
self.p_to_save.append(new_p)
Expand Down
12 changes: 12 additions & 0 deletions book_maker/loader/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ def __init__(
def insert_trans(self, p, text, translation_style="", single_translate=False):
if text is None:
text = ""

# If translation text is empty/None and original text exists,
# preserve the original text instead of inserting empty translation
if not text or not text.strip():
# For empty translations, keep original text
if single_translate:
# In single-translate mode, don't remove original if translation failed
return
else:
# In bilingual mode, don't insert empty translation
return

if (
p.string is not None
and p.string.replace(" ", "").strip() == text.replace(" ", "").strip()
Expand Down
7 changes: 7 additions & 0 deletions book_maker/translator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from book_maker.translator.custom_api_translator import CustomAPI
from book_maker.translator.xai_translator import XAIClient
from book_maker.translator.qwen_translator import QwenTranslator
from book_maker.translator.zhipu_translator import ZhipuTranslator

MODEL_DICT = {
"openai": ChatGPTAPI,
Expand Down Expand Up @@ -40,5 +41,11 @@
"qwen": QwenTranslator,
"qwen-mt-turbo": QwenTranslator,
"qwen-mt-plus": QwenTranslator,
"glm": ZhipuTranslator,
"glm-4-flash": ZhipuTranslator,
"glm-4-air": ZhipuTranslator,
"glm-4-airx": ZhipuTranslator,
"glm-4-plus": ZhipuTranslator,
"glm-4-0520": ZhipuTranslator,
# add more here
}
132 changes: 132 additions & 0 deletions book_maker/translator/zhipu_translator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import re
import time
from openai import OpenAI, APIError
from rich import print
from .chatgptapi_translator import ChatGPTAPI


GLM_MODEL_LIST = [
"GLM-4-Flash",
"GLM-4-Air",
"GLM-4-AirX",
"GLM-4-Plus",
"GLM-4-0520",
]


class ZhipuTranslator(ChatGPTAPI):
"""
Zhipu AI (GLM) translator using OpenAI-compatible API
Supports GLM-4 series models including free tier GLM-4-Flash
API documentation: https://open.bigmodel.cn/dev/api
"""

def __init__(self, key, language, api_base=None, **kwargs) -> None:
super().__init__(key, language, **kwargs)
self.model_list = [GLM_MODEL_LIST[0]] # Default to GLM-4-Flash
self.model = GLM_MODEL_LIST[0] # Set default model
self.api_url = str(api_base) if api_base else "https://open.bigmodel.cn/api/paas/v4/"
self.openai_client = OpenAI(api_key=next(self.keys), base_url=self.api_url)

def rotate_key(self):
"""Rotate API key for load balancing"""
try:
new_key = next(self.keys)
self.openai_client = OpenAI(api_key=new_key, base_url=self.api_url)
except StopIteration:
pass

def rotate_model(self):
"""Set the current model from model list"""
self.model = self.model_list[0]

def translate(self, text, needprint=True):
"""
Override translate method to handle Zhipu-specific content filter errors

When content is filtered (error code 1301), return original text and continue
instead of crashing the entire translation process.
"""
start_time = time.time()

if needprint:
print(re.sub(r"\n{3,}", "\n\n", text))

attempt_count = 0
max_attempts = 3
t_text = ""

while attempt_count < max_attempts:
try:
t_text = self.get_translation(text)
break
except APIError as e:
# Check if this is a content filter error (Zhipu error code 1301)
error_message = str(e)
if "'code': '1301'" in error_message or "敏感内容" in error_message:
print(
f"[yellow]⚠ Content filter triggered - skipping this paragraph[/yellow]"
)
print(
f"[dim]Zhipu AI detected potentially sensitive content. Using original text.[/dim]"
)
t_text = text # Return original text
break
else:
# For other API errors, use default handling
print(f"[red]API Error: {error_message}[/red]")
attempt_count += 1
if attempt_count >= max_attempts:
print(
f"[red]Translation failed after {max_attempts} attempts. Using original text.[/red]"
)
t_text = text
break
time.sleep(1)
except Exception as e:
print(f"[yellow]Translation error: {str(e)}[/yellow]")
print(f"[dim]Using original text for this paragraph.[/dim]")
t_text = text # Return original text instead of None
break

if needprint and t_text and t_text != text:
print("[bold green]" + re.sub(r"\n{3,}", "\n\n", t_text) + "[/bold green]")

elapsed_time = time.time() - start_time
if needprint:
print(f"[dim]Translation time: {elapsed_time:.2f}s[/dim]")

return t_text

def set_glm_model(self, model_name):
"""
Set specific GLM model

Args:
model_name: Model identifier (e.g., 'glm-4-flash', 'glm', etc.)
"""
# Map model identifiers to actual model names
model_mapping = {
"glm": "GLM-4-Flash",
"glm-4-flash": "GLM-4-Flash",
"glm-4-air": "GLM-4-Air",
"glm-4-airx": "GLM-4-AirX",
"glm-4-plus": "GLM-4-Plus",
"glm-4-0520": "GLM-4-0520",
}

actual_model = model_mapping.get(model_name.lower())

if actual_model and actual_model in GLM_MODEL_LIST:
self.model_list = [actual_model]
self.model = actual_model
print(f"[blue]GLM model set to: {actual_model}[/blue]")
else:
# Fallback to default
self.model_list = [GLM_MODEL_LIST[0]]
self.model = GLM_MODEL_LIST[0]
print(
f"[yellow]Invalid GLM model: {model_name}. Using default: {self.model}[/yellow]"
)

return self.model
127 changes: 127 additions & 0 deletions docs/plans/2025-11-08-zhipu-glm-support-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# 智谱AI (GLM) 模型支持设计方案

**日期:** 2025-11-08
**状态:** 已批准

## 概述

为bilingual_book_maker添加智谱AI的GLM系列模型支持,使用OpenAI兼容接口实现。

## 设计目标

- 支持智谱AI的GLM系列模型(GLM-4-Flash等)
- 复用现有的CLI参数设计(--model, --api_base)
- 与项目现有架构保持一致
- 提供免费模型选项(GLM-4-Flash)

## 架构设计

### 核心组件

1. **新建翻译器类**:`book_maker/translator/zhipu_translator.py`
- 继承自 `Base` 翻译器
- 使用OpenAI Python SDK调用兼容接口
- API Base: `https://open.bigmodel.cn/api/paas/v4/`

2. **集成方式**:独立文件(与xai、groq模式一致)

### 支持的模型

| 模型标识 | 模型名称 | 说明 |
|---------|---------|------|
| glm | GLM-4-Flash | 默认模型,通用入口 |
| glm-4-flash | GLM-4-Flash | 高性能免费模型 |
| glm-4-air | GLM-4-Air | 轻量级模型 |
| glm-4-airx | GLM-4-AirX | 增强版轻量模型 |
| glm-4-plus | GLM-4-Plus | 最强模型(付费) |
| glm-4-0520 | GLM-4-0520 | 特定版本 |

## 参数设计

### 新增参数

- **命令行参数**:`--glm_key`
- 用途:传递智谱API密钥
- 示例:`--glm_key sk-xxx`

- **环境变量**:`BBM_GLM_API_KEY`
- 优先级:环境变量 < 命令行参数

### 复用参数

- **--model / -m**:选择模型(已存在)
- **--api_base**:自定义API地址(已存在)
- **--temperature**:控制输出随机性(已存在)
- **--prompt**:自定义翻译提示词(已存在)
- **--proxy**:HTTP代理设置(已存在)

## 实现清单

### 1. 新建文件

**文件**:`book_maker/translator/zhipu_translator.py`

**类设计**:
```python
class ZhipuTranslator(Base):
- API Base默认:https://open.bigmodel.cn/api/paas/v4/
- 默认模型:GLM-4-Flash
- 支持密钥轮换
- 支持自定义prompt
- 支持温度参数
- 错误重试机制(最多3次)
```

### 2. 修改文件

**文件**:`book_maker/translator/__init__.py`
- 导入 `ZhipuTranslator`
- 在 `MODEL_DICT` 添加6个模型映射

**文件**:`book_maker/cli.py`
- 添加 `--glm_key` 参数定义(约第193行)
- 在 model choices 添加6个GLM模型(约第214行)
- 添加GLM API密钥处理逻辑(约第479行)
- 添加GLM模型设置逻辑(约第601行)

## 错误处理

1. **API密钥缺失**:抛出异常并提示用户
2. **模型名称无效**:回退到GLM-4-Flash + 警告
3. **API调用失败**:重试3次,失败返回原文
4. **网络超时**:60秒超时,超时后重试
5. **速率限制**:捕获429错误,等待后重试

## 使用示例

```bash
# 使用命令行参数
python make_book.py --book_name book.epub -m glm-4-flash --glm_key sk-xxx

# 使用环境变量
export BBM_GLM_API_KEY=sk-xxx
python make_book.py --book_name book.epub -m glm

# 使用自定义API地址
python make_book.py --book_name book.epub -m glm-4-flash --glm_key sk-xxx --api_base https://custom-url

# 使用代理
python make_book.py --book_name book.epub -m glm --glm_key sk-xxx --proxy http://127.0.0.1:7890

# 测试模式
python make_book.py --book_name book.epub -m glm --glm_key sk-xxx --test --test_num 5
```

## 兼容性

- 支持所有现有功能:--test, --resume, --proxy, --temperature等
- 与现有翻译器接口完全兼容
- 不影响其他翻译器的功能

## 测试计划

1. 基本翻译功能测试
2. 不同模型切换测试
3. 错误处理测试(无效密钥、网络错误等)
4. 参数组合测试(proxy、temperature、prompt等)
5. 多密钥轮换测试
Loading
Loading