Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
194a6a8206 | ||
|
|
d38de335bd | ||
|
|
f2d3de3659 | ||
|
|
464a6c79dc |
@@ -1,5 +1,21 @@
|
||||
# 更新记录
|
||||
|
||||
## v2.1.8(2026-10-26)
|
||||
|
||||
### 新增「适应高度」开关
|
||||
|
||||
- 提示词一多,英文标签区就卡在写死的 360 像素上限里,只能内部滚动,把节点拉大也没用。侧边栏「标签管理」的明细表上方(「颜色」按钮右边)多了一个「适应高度」滑动开关,跟着所选节点走——切到别的节点就显示那个节点自己的状态。
|
||||
- 打开后:面板不再被固定上限锁住,多出来的高度全部给英文标签区,提示词能多显示很多行;开关打开时节点会自动撑高一次(按被压住的内容量撑),之后想拉多大多大。
|
||||
- 关掉就回到原来的自动高度。开关状态跟着节点保存,重开工作流还在。
|
||||
- 默认关闭,提示词少的时候和以前完全一样。
|
||||
|
||||
### 「优化」按模式分派,新增自然语言规则
|
||||
|
||||
- 节点上的「优化为 Anima」改名为「优化」:标签模式下和以前一样,整理英文标签流、只接受英文;模式切到「自然语言」后改用新的自然语言规则,输出完整英文句子,中文也可以直接优化(会先译成英文,状态栏会提示一句)。
|
||||
- 助手设置里 OpenAI 兼容与 Ollama 各多一条「自然语言规则」,可以分别自定义,也能单独恢复默认。
|
||||
- 模式切到「自然语言」时「按 Anima 顺序排序」自动隐藏——整段句子没有标签流可排;已经排过序时按钮会保留,方便点「取消排序」。
|
||||
- 手动选的「自然语言」也算,不用非得自动检测出来;模式选择现在跟着节点保存,重开工作流不用重选。
|
||||
|
||||
## v2.1.5(2026-10-13)
|
||||
|
||||
### 可信度徽章改成按词条整理程度分档
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ComfyUI 双语提示词管理器 v2.1.5
|
||||
# ComfyUI 双语提示词管理器 v2.1.8
|
||||
|
||||
> ComfyUI 双语提示词节点:把英文提示词拆成可点击的标签,边写边看中文解释、调顺序、试效果。
|
||||
|
||||
@@ -73,8 +73,10 @@
|
||||
|
||||
| 能力 | 说明 |
|
||||
|---|:-:|
|
||||
| **四种处理方式** | 仅翻译 / 翻译并优化 / 优化为 Anima / 按 Anima 官方顺序整理(不新增标签,只重排分段) |
|
||||
| **自动识别模式** | 自动区分纯标签、纯自然语言、混合文本和翻译指令,不用手选模式 |
|
||||
| **四种处理方式** | 仅翻译 / 翻译并优化 / 优化 / 按 Anima 官方顺序整理(不新增标签,只重排分段) |
|
||||
| **优化按模式分派** | 标签模式下「优化」整理现有英文标签流(只吃英文);自然语言模式下改写为英文自然语句,中文会先译成英文。两种模式各用一套规则,可在助手设置里分别自定义 |
|
||||
| **自动识别模式** | 自动区分纯标签、纯自然语言、混合文本和翻译指令,也可以在明细表上方手动指定;指定的模式跟着节点保存 |
|
||||
| **自然语言模式** | 模式切到「自然语言」后,「按 Anima 顺序排序」自动隐藏——整段句子没有标签流可排 |
|
||||
| **提示词诊断** | 自动标出可疑权重、重复标签、语法问题 |
|
||||
| **快捷添加** | 底部的快捷输入框:写中文自动翻译成英文标签后追加,写英文标签直接追加;`Enter` 添加、`Shift+Enter` 换行、`Ctrl+Enter` 翻译并优化 |
|
||||
|
||||
@@ -85,7 +87,7 @@
|
||||
| **多来源词库** | 内置词库 + 个人词库 + 社区词库包,每条解释都会标注来源 |
|
||||
| **概念搜索** | 搜「丝袜」能返回连裤袜、长筒袜、过膝袜等相关标签,支持滚动分页加载 |
|
||||
| **大型 Danbooru 词库(可选)** | 按需安装,覆盖面更广,不装也不影响其它功能 |
|
||||
| **翻译服务可切换** | 纯词库(离线、免费)/ 百度翻译 / OpenAI 兼容 API / Ollama。翻译与解释可选任意一种;「翻译并优化」「优化为 Anima」恒定走 AI |
|
||||
| **翻译服务可切换** | 纯词库(离线、免费)/ 百度翻译 / OpenAI 兼容 API / Ollama。翻译与解释可选任意一种;「翻译并优化」「优化」恒定走 AI |
|
||||
| **密钥互不干扰** | AI 与百度密钥独立留存,切换翻译服务不会清空;均不写入工作流、不返回浏览器 |
|
||||
|
||||
### 上游提示词接管
|
||||
@@ -134,7 +136,7 @@
|
||||
|
||||
节点上可以大致分成四块:
|
||||
|
||||
1. **英文原文(实际输出)**——节点真正输出给下游的文本。上面有「编辑文本 / 完成」「清空」「插入换行」「收藏」「优化为 Anima」「按 Anima 顺序排序」几个按钮。
|
||||
1. **英文原文(实际输出)**——节点真正输出给下游的文本。上面有「编辑文本 / 完成」「清空」「插入换行」「收藏」「优化」「按 Anima 顺序排序」几个按钮。
|
||||
2. **中文对照区**——英文标签逐条对应的中文,只用于阅读,不进入输出,也不会传给下游。
|
||||
3. **明细表**——每行是一个标签:来源、翻译、权重、分类,右侧按行状态给出操作(翻译 / 确认并存 / 重新翻译 / 编辑中文 / 收藏 / 复制 / 删除个人词条)。上方有搜索框、模式选择、颜色模式、筛选(全部 / 未知 / 机器翻译 / 个人词库 / 收藏)和问题提示。
|
||||
4. **快捷添加**——底部输入框,中文进英文出(自动翻译后追加),英文则直接追加。
|
||||
@@ -193,11 +195,11 @@ git clone --depth 1 https://github.com/mengshengzhijie/ComfyUI-Bilingual-Prompt-
|
||||
|---|---|
|
||||
| **翻译服务** | 翻译 / 解释按此选择:纯词库(离线、免费)/ 百度翻译(需自行申请开放平台的 APP ID 与密钥)/ OpenAI 兼容接口 / Ollama(本地)。选后面两个时,它们本身就是下面的 AI 服务 |
|
||||
| **百度翻译** | 独立配置 APP ID 与密钥,切换翻译服务不会清空 |
|
||||
| **AI 服务** | 「翻译并优化」「优化为 Anima」恒定走这里。翻译服务选了 OpenAI 兼容 / Ollama 时,下面直接就是那个后端的配置;选纯词库 / 百度翻译时,这里多出一个「AI 后端」下拉,决定优化用哪一套。两个后端各存一套配置,切换不清空: |
|
||||
| **AI 服务** | 「翻译并优化」「优化」恒定走这里。翻译服务选了 OpenAI 兼容 / Ollama 时,下面直接就是那个后端的配置;选纯词库 / 百度翻译时,这里多出一个「AI 后端」下拉,决定优化用哪一套。两个后端各存一套配置,切换不清空: |
|
||||
| ↳ OpenAI 兼容 | API 地址、模型、可选 API Key、温度、超时,另有「使用 LM Studio 本地预设」一键填入 `1234/v1`。远端地址必须 https,本机回环(127.0.0.1 / localhost)才允许 http;内网、保留、链路本地地址一律拒绝 |
|
||||
| ↳ Ollama | API 地址、模型、温度、超时,没有密钥,另有「使用 Ollama 本地预设」一键填入 `11434`。地址只能是本机回环地址,其它一律拒绝 |
|
||||
|
||||
两个后端还各有一套规则(翻译 / 翻译并优化 / 优化):本地小模型与云端大模型要写的提示词往往不一样,分开存可以随时切换后端而不用重改规则。地址不合规时会在发出请求前直接报错。
|
||||
两个后端还各有一套规则(翻译 / 翻译并优化 / 优化 / 自然语言):本地小模型与云端大模型要写的提示词往往不一样,分开存可以随时切换后端而不用重改规则。其中「自然语言」那套只在自然语言模式下被「优化」使用,结果按完整英文句子保留标点,不套 Anima 的标点限制。地址不合规时会在发出请求前直接报错。
|
||||
|
||||
### 大型词库(可选)
|
||||
|
||||
|
||||
+41
-2
@@ -46,11 +46,18 @@ DEFAULT_OPTIMIZATION_RULE = """你是 Anima 模型英文提示词优化器。输
|
||||
对于明确为成年人的合法成人内容,准确保留并规范相关成人标签,不弱化、不含糊替换,也不主动增加原文没有的成人内容;不得将未成年人或年龄不明确的人物成人化。
|
||||
标签使用英文逗号分隔;自然语言只放在末尾并使用英文句点分隔。仅使用 Anima 支持的英文标点和语法:逗号、句点、圆括号与冒号权重、@ 画师前缀、下划线、连字符、撇号、百分号,以及尖括号 LoRA 语法。"""
|
||||
|
||||
DEFAULT_NATURAL_RULE = """你是面向现代生图模型(Krea、FLUX、Qwen-Image 等)的英文自然语言提示词助手。把用户输入整理成一段通顺、具体、可直接使用的英文描述;只返回最终结果,不输出 Markdown、标题、引号、解释、警告或免责声明。
|
||||
输入是中文、英文或中英混合都可以:含中文时先忠实译为英文再整理表达,纯英文时只做表达优化,不改变原意。输入是逗号分隔的标签时,先理解这些标签的含义,再改写成一段连贯的英文自然语言描述。
|
||||
保留主体、动作与状态、外貌与服装、环境与背景、光线、镜头与构图、风格与画材、氛围与情绪;原文没有给出的要素不要凭空添加,也不要删掉原文已经写明的限制。
|
||||
严格保留人物数量、成年人身份、角色名、作品名、否定关系、左右位置和安全等级,不得擅自换人、增加人物或改变主题。
|
||||
对于明确为成年人的合法成人内容,准确保留相关描述,不弱化、不含糊替换,也不主动增加原文没有的成人内容;不得将未成年人或年龄不明确的人物成人化。
|
||||
用完整的英文句子输出,按语法正常使用逗号、句点和连接词;不要写成逗号分隔的标签列表,也不要使用括号权重、@ 画师前缀或尖括号语法。"""
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"schema_version": 4,
|
||||
# 翻译/解释走哪个服务;OpenAI 兼容与 Ollama 本身就是两个平级选项
|
||||
"translate_service": "dictionary", # dictionary | baidu | openai_compatible | ollama
|
||||
# 选词库/百度时翻译不走 AI,但「翻译并优化」「优化为 Anima」恒走 AI —— 这时用它决定读哪一套
|
||||
# 选词库/百度时翻译不走 AI,但「翻译并优化」「优化」恒走 AI —— 这时用它决定读哪一套
|
||||
"ai_provider": "openai_compatible", # openai_compatible | ollama
|
||||
# OpenAI 兼容:远端必须 https,本机回环 http 也放行(本地 LM Studio 之类)
|
||||
"openai_base_url": "",
|
||||
@@ -76,9 +83,11 @@ DEFAULT_CONFIG = {
|
||||
"openai_translation_rule": DEFAULT_TRANSLATION_RULE,
|
||||
"openai_translate_optimize_rule": DEFAULT_TRANSLATE_OPTIMIZE_RULE,
|
||||
"openai_optimization_rule": DEFAULT_OPTIMIZATION_RULE,
|
||||
"openai_natural_rule": DEFAULT_NATURAL_RULE,
|
||||
"ollama_translation_rule": DEFAULT_TRANSLATION_RULE,
|
||||
"ollama_translate_optimize_rule": DEFAULT_TRANSLATE_OPTIMIZE_RULE,
|
||||
"ollama_optimization_rule": DEFAULT_OPTIMIZATION_RULE,
|
||||
"ollama_natural_rule": DEFAULT_NATURAL_RULE,
|
||||
}
|
||||
|
||||
# 规则按后端分成两套,遍历时统一用这两个前缀
|
||||
@@ -86,7 +95,7 @@ AI_PROVIDER_PREFIXES = {"openai_compatible": "openai", "ollama": "ollama"}
|
||||
# translate_service 里属于 AI 的两个值,与 ai_provider 共用同一套前缀
|
||||
AI_SERVICES = frozenset(AI_PROVIDER_PREFIXES)
|
||||
TRANSLATE_SERVICES = frozenset({"dictionary", "baidu"}) | AI_SERVICES
|
||||
RULE_KEYS = ("translation_rule", "translate_optimize_rule", "optimization_rule")
|
||||
RULE_KEYS = ("translation_rule", "translate_optimize_rule", "optimization_rule", "natural_rule")
|
||||
|
||||
BAIDU_TRANSLATE_ENDPOINT = "https://fanyi-api.baidu.com/api/trans/vip/translate"
|
||||
|
||||
@@ -289,6 +298,35 @@ def sanitize_anima_prompt(value):
|
||||
return text
|
||||
|
||||
|
||||
def sanitize_natural_prompt(value):
|
||||
"""自然语言优化结果的清洗。
|
||||
|
||||
与 sanitize_anima_prompt 的区别:这里要保留完整句子,所以既不把句点改成逗号,
|
||||
也不做 Anima 标点白名单收窄——只挡代码块、转义符、列表符号和残留中文。
|
||||
"""
|
||||
text = str(value or "").strip()
|
||||
fenced = re.fullmatch(r"```(?:text|plaintext)?\s*([\s\S]*?)\s*```", text, re.IGNORECASE)
|
||||
text = fenced.group(1).strip() if fenced else text
|
||||
replacements = {
|
||||
",": ", ", "、": ", ", ";": "; ", "。": ". ",
|
||||
"!": "! ", "?": "? ", ":": ": ",
|
||||
"(": "(", ")": ")", "<": "<", ">": ">", "—": " - ", "–": " - ",
|
||||
"“": '"', "”": '"', """: '"', "‘": "'", "’": "'", "'": "'", "`": "",
|
||||
}
|
||||
for source, target in replacements.items():
|
||||
text = text.replace(source, target)
|
||||
text = re.sub(r"\\+[nrt]", "\n", text)
|
||||
text = text.replace("\\", "")
|
||||
text = re.sub(r"(?m)^\s*(?:[-*•]+|\d+[.)])\s*", "", text)
|
||||
text = re.sub(r"[\r\n]+", " ", text)
|
||||
text = re.sub(r"\s+", " ", text).strip()
|
||||
if re.search(r"[\u3400-\u9fff]", text):
|
||||
raise ValueError("助手返回的自然语言提示词仍包含中文,请调整规则后重试")
|
||||
if not text:
|
||||
raise ValueError("助手返回的自然语言提示词为空")
|
||||
return text
|
||||
|
||||
|
||||
def _default_config_dir():
|
||||
try:
|
||||
import folder_paths
|
||||
@@ -488,6 +526,7 @@ class AssistantStore:
|
||||
defaults[f"default_{prefix}_translation_rule"] = DEFAULT_TRANSLATION_RULE
|
||||
defaults[f"default_{prefix}_translate_optimize_rule"] = DEFAULT_TRANSLATE_OPTIMIZE_RULE
|
||||
defaults[f"default_{prefix}_optimization_rule"] = DEFAULT_OPTIMIZATION_RULE
|
||||
defaults[f"default_{prefix}_natural_rule"] = DEFAULT_NATURAL_RULE
|
||||
return {
|
||||
key: config[key] for key in DEFAULT_CONFIG if key not in ("openai_api_key", "baidu_secret_key")
|
||||
} | {
|
||||
|
||||
@@ -33,10 +33,11 @@
|
||||
- `server.py`
|
||||
- 注册助手、个人词库、词库包和大型词库的本地接口。
|
||||
- 执行同源检查、临时会话令牌校验、请求体限制、助手频率限制和并发限制。
|
||||
- 只在用户主动调用时访问配置的百度翻译或 AI(OpenAI 兼容 / Ollama)服务;"翻译并优化"与"优化为 Anima"恒走 AI,翻译/解释按 `translate_service` 选择。
|
||||
- 只在用户主动调用时访问配置的百度翻译或 AI(OpenAI 兼容 / Ollama)服务;"翻译并优化"与"优化"恒走 AI,翻译/解释按 `translate_service` 选择。
|
||||
- `UpstreamGate` 负责上游输入的挂起:按节点 id 保存一个 future,只能被 `/bpi/upstream/{ack,resume,cancel}` 唤醒。等待循环每 0.5 秒检查一次 ComfyUI 中断,无人接管时到点自动透传。唤醒一律走 `loop.call_soon_threadsafe`——future 属于执行线程的 loop,而路由跑在主线程。
|
||||
- `assistant_store.py`
|
||||
- 保存翻译服务类型(schema v4:`translate_service` = dictionary / baidu / openai_compatible / ollama;后两个同时是 AI 后端,选前两个时由 `ai_provider` 决定优化走哪一套)、两套 AI 配置(OpenAI 兼容 / Ollama 各一套地址、模型、温度、超时与三条规则)、百度 APP ID 与密钥,以及独立留存的 AI Key 与百度密钥。
|
||||
- 保存翻译服务类型(schema v4:`translate_service` = dictionary / baidu / openai_compatible / ollama;后两个同时是 AI 后端,选前两个时由 `ai_provider` 决定优化走哪一套)、两套 AI 配置(OpenAI 兼容 / Ollama 各一套地址、模型、温度、超时与四条规则)、百度 APP ID 与密钥,以及独立留存的 AI Key 与百度密钥。
|
||||
- 四条规则是翻译 / 翻译并优化 / 优化 / 自然语言。前三条产出 Anima 标签流,走 `sanitize_anima_prompt`(句点收成逗号、标点只放行白名单);自然语言那条产出完整英文句子,走 `sanitize_natural_prompt`,两者不能混用。
|
||||
- 读到旧值 `translate_service = "ai"` 时按当时的 `ai_provider` 拆成 `openai_compatible` / `ollama` 并回写。
|
||||
- AI Key 与百度密钥互不干扰:切换翻译服务或 AI 后端不清任一密钥;AI Key 只在地址变更或换机器时清,百度密钥只在显式清除时清。
|
||||
- 旧 v2 配置(单一 `provider`)自动迁移至 v3,密钥与绑定原样搬入 `ai_*` 槽。
|
||||
@@ -117,7 +118,8 @@ ComfyUI 原生 text 控件(工作流真值)
|
||||
|
||||
```text
|
||||
绿色草稿框
|
||||
→ 用户点击仅翻译 / 翻译并优化 / 优化为 Anima
|
||||
→ 用户点击仅翻译 / 翻译并优化 / 优化
|
||||
→ 自然语言模式下「优化」改用 optimize_natural,走自然语言规则
|
||||
→ /bpi/assistant/run
|
||||
→ 纯词库、百度翻译或 AI(OpenAI 兼容 / Ollama)
|
||||
→ 结果写回绿色暂存区
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
## 3. 翻译、优化和同步必须分阶段
|
||||
|
||||
决定:设置“仅翻译”“翻译并优化”“优化为 Anima”“同步到英文输出”四个独立动作。
|
||||
决定:设置“仅翻译”“翻译并优化”“优化”“同步到英文输出”四个独立动作。
|
||||
|
||||
原因:翻译与提示词创作不是同一任务。用户需要看见模型做了什么,并决定是否覆盖实际输出。每个动作使用独立规则,避免一个通用提示词同时承担忠实翻译、扩写和格式整理而行为混乱。
|
||||
原因:翻译与提示词创作不是同一任务。用户需要看见模型做了什么,并决定是否覆盖实际输出。每个动作使用独立规则,避免一个通用提示词同时承担忠实翻译、扩写和格式整理而行为混乱。“优化”会按当前输入模式分派不同规则,见第 19 条。
|
||||
|
||||
## 4. Anima 排序只在按下按钮后执行
|
||||
|
||||
@@ -116,3 +116,13 @@
|
||||
- GitHub 首个公开正式 Release 使用 `v1.0.0`。
|
||||
- 后续发布应使用语义化版本号,不要把内部演进代次和公开版本号混为一谈。
|
||||
- 兼容修复递增补丁号;向后兼容的新功能递增次版本;破坏工作流或数据格式时才递增主版本,并提供迁移说明。
|
||||
|
||||
## 19. “优化”按输入模式分派,而不是再加一个按钮
|
||||
|
||||
决定:节点顶部只保留一个“优化”按钮。标签模式下走 Anima 优化规则(只接受英文),自然语言模式下走独立的自然语言规则(中文先译后整理);模式切到“自然语言”时“按 Anima 顺序排序”隐藏。
|
||||
|
||||
原因:Krea、FLUX 一类新模型直接吃自然语言,再加一个按钮只会让顶部操作越排越长。模式下拉已经能判定当前是标签流还是整段句子,用它分派比让用户记两个按钮省事;排序只重排标签流,对整段句子没有意义,留着反而误导。
|
||||
|
||||
两套规则必须分开:`sanitize_anima_prompt` 会把句点收成逗号并只放行 Anima 标点白名单,完整句子喂进去会被判废,所以自然语言结果改走 `sanitize_natural_prompt`。
|
||||
|
||||
代价:已经排过序时要保留排序按钮,否则“取消排序”的入口会跟着一起消失。另外 mixed(标签 + 自然语言)不算自然语言模式——里面还有标签流可排。
|
||||
|
||||
+149
-22
@@ -57,9 +57,14 @@ import {
|
||||
} from "./bpi_shared.js";
|
||||
|
||||
const NODE_NAME = "BilingualPromptInspector";
|
||||
const EXTENSION_VERSION = "v2.1.5";
|
||||
const EXTENSION_VERSION = "v2.1.8";
|
||||
const PROJECT_URL = "https://github.com/mengshengzhijie/ComfyUI-Bilingual-Prompt-Inspector";
|
||||
const COLLAPSED_WIDGET_FALLBACK_HEIGHT = 390;
|
||||
// 「适应高度」打开后面板能长到的上限。ComfyUI 按 min/max 分配空间,max 必须明显高于 min,
|
||||
// 否则手动拉大节点多出来的高度全变成空白(distributeSpace 只会在 min..max 之间给)。
|
||||
const EXPANDED_WIDGET_MAX_HEIGHT = 1600;
|
||||
// 打开开关时至少给节点加这么高,保证「点了有反应」;内容被压掉更多时按实际差值撑
|
||||
const FILL_HEIGHT_MIN_GROWTH = 200;
|
||||
const COLLAPSED_NODE_MIN_HEIGHT = 360;
|
||||
const IMPORT_POLICIES = [
|
||||
{ value: "changed", label: "Import: on content change" },
|
||||
@@ -132,17 +137,25 @@ function createPanel(node, textWidget) {
|
||||
const insertBreakButton = element("button", "bpi-button bpi-mini", "Insert line break");
|
||||
// 中文不再单独占一块:每个英文标签下面直接跟一行中文,构成一张双语小卡。
|
||||
// 翻译类动作已移到侧边栏,节点里只保留「直接作用于英文提示词」的这两个。
|
||||
const optimizeChineseButton = element("button", "bpi-button bpi-mini", "Optimize to Anima");
|
||||
const optimizeButton = element("button", "bpi-button bpi-mini", "Optimize");
|
||||
const sortPromptButton = element("button", "bpi-button bpi-mini", "Sort by Anima order");
|
||||
// 「适应高度」:把面板高度从写死的上限里放出来,让提示词区跟着节点一起变高。
|
||||
// 做成滑动开关(和设置面板里同款,只是用主题色而不是危险色),放在明细表上方的筛选栏里。
|
||||
const fillSwitchRow = element("label", "bpi-switch-row bpi-switch-accent");
|
||||
const fillSwitch = element("input", "bpi-switch");
|
||||
fillSwitch.type = "checkbox";
|
||||
const fillTrack = element("span", "bpi-switch-track");
|
||||
fillTrack.appendChild(element("span", "bpi-switch-thumb"));
|
||||
fillSwitchRow.append(fillSwitch, fillTrack, element("span", "bpi-switch-text", "Fit height"));
|
||||
editEnglishButton.type = "button";
|
||||
clearEnglishButton.type = "button";
|
||||
favoriteButton.type = "button";
|
||||
insertBreakButton.type = "button";
|
||||
optimizeChineseButton.type = "button";
|
||||
optimizeButton.type = "button";
|
||||
sortPromptButton.type = "button";
|
||||
setTitle(favoriteButton, "Save the current English prompt to favorites in the user directory (a reference image can be attached)");
|
||||
setTitle(insertBreakButton, "Insert a line break after the selected tag, or at the end when no tag is selected");
|
||||
englishHead.append(englishTitle, englishHint, editEnglishButton, clearEnglishButton, insertBreakButton, favoriteButton, optimizeChineseButton, sortPromptButton);
|
||||
englishHead.append(englishTitle, englishHint, editEnglishButton, clearEnglishButton, insertBreakButton, favoriteButton, optimizeButton, sortPromptButton);
|
||||
const englishTokenView = element("div", "bpi-english-token-view bpi-hidden");
|
||||
englishTokenView.tabIndex = 0;
|
||||
englishTokenView.setAttribute("role", "textbox");
|
||||
@@ -274,6 +287,13 @@ function createPanel(node, textWidget) {
|
||||
option.value = value;
|
||||
modeSelect.appendChild(option);
|
||||
}
|
||||
// 模式跟着节点存:工作流保存后重开仍是上次选的那一档,没存过就回到自动检测
|
||||
const storedMode = () => {
|
||||
const value = node.properties?.bpiMode;
|
||||
return ["tags", "natural"].includes(value) ? value : "auto";
|
||||
};
|
||||
// 「适应高度」也跟着节点存:工作流重开后仍是上次那个状态
|
||||
const storedFillHeight = () => node.properties?.bpiFillHeight === true;
|
||||
|
||||
const state = {
|
||||
data: { tags: [], builtin: [], user: [], packs: [] },
|
||||
@@ -286,7 +306,8 @@ function createPanel(node, textWidget) {
|
||||
selected: new Set(),
|
||||
lastText: null,
|
||||
renderTimer: null,
|
||||
modePreference: "auto",
|
||||
modePreference: storedMode(),
|
||||
fillHeight: storedFillHeight(),
|
||||
modeInfo: { mode: "tags", reason: "Empty input", confidence: "high" },
|
||||
issues: [],
|
||||
localOverrides: new Map(),
|
||||
@@ -366,6 +387,12 @@ function createPanel(node, textWidget) {
|
||||
};
|
||||
const autoFitGreenArea = (target, minimum) => {
|
||||
if (!target || target.classList.contains("bpi-hidden")) return;
|
||||
// 适应高度模式下高度交给 flex;这里再写死 height 会把刚放开的上限又按回去
|
||||
if (state.fillHeight) {
|
||||
target.style.height = "";
|
||||
target.style.overflowY = "auto";
|
||||
return;
|
||||
}
|
||||
const maximum = 360;
|
||||
target.style.height = "auto";
|
||||
const height = Math.max(minimum, Math.min(maximum, Math.ceil(target.scrollHeight + 2)));
|
||||
@@ -439,9 +466,20 @@ function createPanel(node, textWidget) {
|
||||
if (state.resizeFrame !== null) cancelAnimationFrame(state.resizeFrame);
|
||||
state.resizeFrame = requestAnimationFrame(() => {
|
||||
state.resizeFrame = null;
|
||||
const width = Math.max(node.size?.[0] ?? 0, 590);
|
||||
// 适应高度模式:高度归用户,只同步宽度。这里绝不能按内容重算——
|
||||
// 英文区一变高就会推高自动高度、再推高节点、英文区又更高,越滚越大。
|
||||
if (state.fillHeight) {
|
||||
if (Math.abs((node.size?.[0] ?? 0) - width) > 0.5) {
|
||||
node.setSize?.([width, Number(node.size?.[1]) || COLLAPSED_NODE_MIN_HEIGHT]);
|
||||
node.graph?.change?.();
|
||||
}
|
||||
node.graph?.setDirtyCanvas?.(true, true);
|
||||
app.graph?.setDirtyCanvas?.(true, true);
|
||||
return;
|
||||
}
|
||||
state.collapsedWidgetHeight = measureCollapsedWidgetHeight();
|
||||
panel.style.setProperty("--bpi-collapsed-height", `${state.collapsedWidgetHeight}px`);
|
||||
const width = Math.max(node.size?.[0] ?? 0, 590);
|
||||
// 稳定基线 = 标题栏 + 原生 text 控件的真实内容高度。
|
||||
// 绝不能读 inspectorWidget().y(= bilingual_inspector.y)——那是 ComfyUI arrange
|
||||
// 从 text.computedHeight 派生的,而 text.computedHeight 在「setSize 变高 → 文本框
|
||||
@@ -467,16 +505,74 @@ function createPanel(node, textWidget) {
|
||||
fallbackBaseHeight: state.nodeBaseHeight ?? 96,
|
||||
minimumHeight: COLLAPSED_NODE_MIN_HEIGHT,
|
||||
});
|
||||
// 高度只补不缩:用户把节点拉大之后,任何一次 render 都不该把它拽回自动高度,
|
||||
// 否则「拉大 → 改一个字 → 被打回原形」,拉大等于白拉。宽度仍按最小可用宽度收敛。
|
||||
const currentHeight = Number(node.size?.[1]) || 0;
|
||||
const nextHeight = Math.max(targetHeight, currentHeight);
|
||||
const sizeChanged = Math.abs((node.size?.[0] ?? 0) - width) > 0.5
|
||||
|| Math.abs((node.size?.[1] ?? 0) - targetHeight) > 0.5;
|
||||
|| Math.abs(currentHeight - nextHeight) > 0.5;
|
||||
if (sizeChanged) {
|
||||
node.setSize?.([width, targetHeight]);
|
||||
node.setSize?.([width, nextHeight]);
|
||||
node.graph?.change?.();
|
||||
}
|
||||
node.graph?.setDirtyCanvas?.(true, true);
|
||||
app.graph?.setDirtyCanvas?.(true, true);
|
||||
});
|
||||
};
|
||||
|
||||
// 量出英文标签区「没被 360 压住时」的真实高度:临时解除上限读 scrollHeight 再恢复
|
||||
const measureNaturalEnglishHeight = () => {
|
||||
const previousHeight = englishTokenView.style.height;
|
||||
const previousMax = englishTokenView.style.maxHeight;
|
||||
englishTokenView.style.height = "auto";
|
||||
englishTokenView.style.maxHeight = "none";
|
||||
const natural = englishTokenView.scrollHeight;
|
||||
englishTokenView.style.height = previousHeight;
|
||||
englishTokenView.style.maxHeight = previousMax;
|
||||
return natural;
|
||||
};
|
||||
|
||||
// 打开开关时把节点撑高一次,撑的量 = 被压掉的内容高度,但至少给一个看得出变化的量
|
||||
const growNodeForFill = () => {
|
||||
if (!hasNodeSize()) return;
|
||||
const natural = measureNaturalEnglishHeight();
|
||||
const extra = Math.max(FILL_HEIGHT_MIN_GROWTH, Math.ceil(natural - englishTokenView.offsetHeight));
|
||||
const height = Math.min((Number(node.size?.[1]) || COLLAPSED_NODE_MIN_HEIGHT) + extra, 2400);
|
||||
node.setSize?.([Math.max(node.size?.[0] ?? 0, 590), height]);
|
||||
node.graph?.change?.();
|
||||
};
|
||||
|
||||
const setFillHeight = (next) => {
|
||||
const fill = Boolean(next);
|
||||
if (fill === state.fillHeight) return;
|
||||
state.fillHeight = fill;
|
||||
if (!node.properties || typeof node.properties !== "object") node.properties = {};
|
||||
node.properties.bpiFillHeight = fill;
|
||||
panel.classList.toggle("bpi-fill", fill);
|
||||
fillSwitch.checked = fill;
|
||||
setTitle(fillSwitchRow, fill
|
||||
? "Back to the automatic height; the prompt area stops following the node"
|
||||
: "Let the prompt area grow with the node instead of being capped");
|
||||
if (fill) {
|
||||
// 先放开限制再撑高,撑完重排一次让英文区吃到新空间
|
||||
growNodeForFill();
|
||||
requestNodeResize();
|
||||
} else {
|
||||
state.collapsedWidgetHeight = measureCollapsedWidgetHeight();
|
||||
panel.style.setProperty("--bpi-collapsed-height", `${state.collapsedWidgetHeight}px`);
|
||||
if (hasNodeSize()) {
|
||||
const baseHeight = state.nodeBaseHeight ?? 96;
|
||||
node.setSize?.([Math.max(node.size?.[0] ?? 0, 590), Math.max(inspectorNodeTargetHeight({
|
||||
baseHeight,
|
||||
targetWidgetHeight: state.collapsedWidgetHeight,
|
||||
fallbackBaseHeight: state.nodeBaseHeight ?? 96,
|
||||
minimumHeight: COLLAPSED_NODE_MIN_HEIGHT,
|
||||
}), COLLAPSED_NODE_MIN_HEIGHT)]);
|
||||
node.graph?.change?.();
|
||||
}
|
||||
}
|
||||
render();
|
||||
};
|
||||
const isFavorite = (english) => state.preferences.favorites.includes(normalizeKey(english));
|
||||
const changeFavorite = (english) => {
|
||||
state.preferences = toggleFavorite(state.preferences, english);
|
||||
@@ -2154,30 +2250,36 @@ function createPanel(node, textWidget) {
|
||||
|
||||
const setAssistantBusy = (busy) => {
|
||||
state.assistantBusy = Boolean(busy);
|
||||
optimizeChineseButton.disabled = state.assistantBusy;
|
||||
optimizeButton.disabled = state.assistantBusy;
|
||||
sortPromptButton.disabled = state.assistantBusy;
|
||||
};
|
||||
|
||||
// 「优化为 Anima」只处理英文提示词本身(中文翻译在侧边栏做),
|
||||
// 自然语言模式下没有标签流可以重排,「按 Anima 排序」整个不出现;
|
||||
// 「优化」也随之改走自然语言规则(可以吃中文),标签模式维持「只处理英文」。
|
||||
const isNaturalPrompt = () => state.modeInfo.mode === "natural";
|
||||
|
||||
// 结果不直接写回,先弹预览让用户确认。
|
||||
const runAnimaOptimize = async () => {
|
||||
const runOptimize = async () => {
|
||||
if (state.assistantBusy) return;
|
||||
const source = String(textWidget.value ?? "").trim();
|
||||
if (!source) {
|
||||
setStatus("English prompt is empty; nothing to optimize", "error");
|
||||
return;
|
||||
}
|
||||
if (containsChinese(source)) {
|
||||
setStatus("“Optimize to Anima” only processes English; translate Chinese in the sidebar first", "error");
|
||||
const label = "Optimize";
|
||||
const natural = isNaturalPrompt();
|
||||
const hasChinese = containsChinese(source);
|
||||
if (hasChinese && !natural) {
|
||||
setStatus("“Optimize” only processes English in tag mode; translate Chinese in the sidebar first", "error");
|
||||
return;
|
||||
}
|
||||
const label = "Optimize to Anima";
|
||||
setAssistantBusy(true);
|
||||
setStatus(`Running “${label}”…`, "busy");
|
||||
// 自然语言模式是允许中文的,这里明说一句会顺带翻译,免得用户以为漏处理了
|
||||
setStatus(`Running “${label}”…${hasChinese ? " (Chinese detected; translating to English first)" : ""}`, "busy");
|
||||
try {
|
||||
const output = await runInspectorAssistant("optimize", source);
|
||||
const punctuationError = animaPunctuationError(output);
|
||||
openTextPreview("Optimize to Anima", output, null, label);
|
||||
const output = await runInspectorAssistant(natural ? "optimize_natural" : "optimize", source);
|
||||
const punctuationError = natural ? "" : animaPunctuationError(output);
|
||||
openTextPreview(label, output, null, label);
|
||||
if (punctuationError) {
|
||||
setStatus(`“${punctuationError}”; please fix it in the preview before confirming`, "error");
|
||||
} else {
|
||||
@@ -2208,6 +2310,9 @@ function createPanel(node, textWidget) {
|
||||
// 排序写入之后按钮就变成「取消」:点一下把提示词和分类视图一起还原到排序前。
|
||||
const updateSortButton = () => {
|
||||
const sorted = Boolean(state.animaSortSnapshot);
|
||||
// 排序只重排标签流;整段自然语言没有可排的东西,按钮整个不出现。
|
||||
// 但已经排过序时要留着,否则「取消排序」的入口会跟着一起消失。
|
||||
sortPromptButton.hidden = isNaturalPrompt() && !state.animaSortSnapshot;
|
||||
setText(sortPromptButton, sorted ? "Cancel Anima order sort" : "Sort by Anima order");
|
||||
setTitle(sortPromptButton, sorted
|
||||
? "Restore the prompt as it was before the Anima sort"
|
||||
@@ -2886,8 +2991,9 @@ function createPanel(node, textWidget) {
|
||||
state.lastText = text;
|
||||
// 排序完又手动改了文本,「回到排序前」会把这些改动一起吞掉,取消入口随之失效
|
||||
if (state.animaSortSnapshot && text.trim() !== state.animaSortSnapshot.after) state.animaSortSnapshot = null;
|
||||
updateSortButton();
|
||||
state.modeInfo = detectInputMode(text, state.modePreference);
|
||||
// 按钮显隐依赖 modeInfo,必须排在它更新之后
|
||||
updateSortButton();
|
||||
state.tokens = parseTokens(text);
|
||||
queueLargeLookup(state.tokens);
|
||||
// 重复分组要在渲染两个视图之前算好,标签区和明细表都吃这份数据
|
||||
@@ -3408,6 +3514,9 @@ function createPanel(node, textWidget) {
|
||||
}));
|
||||
});
|
||||
filtersBar.appendChild(colorButton);
|
||||
// 「适应高度」跟着明细表走:明细表被侧边栏「标签管理」复用,开关也就在那边,
|
||||
// 跟着所选节点换(每个节点一份实例 + 状态存在 node.properties 上)
|
||||
filtersBar.appendChild(fillSwitchRow);
|
||||
leftTools.append(
|
||||
button("Add tag", () => openTagDialog({}, refreshDictionary), "bpi-primary"),
|
||||
translateAllButton,
|
||||
@@ -3424,6 +3533,7 @@ function createPanel(node, textWidget) {
|
||||
button("Refresh dictionary", refreshDictionary),
|
||||
);
|
||||
toolbar.append(leftTools, rightTools);
|
||||
modeSelect.value = state.modePreference;
|
||||
searchLine.append(modeSelect, searchLabel, search);
|
||||
summary.append(counts, modeInfo, status);
|
||||
detailsBody.append(toolbar, searchLine, results, summary, filtersBar, colorPanel, machineBatchBar, issuesPanel, tableSearch, table, detailsHiddenBar);
|
||||
@@ -3557,8 +3667,14 @@ function createPanel(node, textWidget) {
|
||||
event.stopPropagation();
|
||||
action();
|
||||
});
|
||||
setTitle(optimizeChineseButton, "Only optimizes existing English; does not translate");
|
||||
bindMirrorAction(optimizeChineseButton, runAnimaOptimize);
|
||||
setTitle(optimizeButton, "Optimize with the rule for the current mode: tag mode tidies the tag flow, natural language mode rewrites it as English sentences");
|
||||
bindMirrorAction(optimizeButton, runOptimize);
|
||||
fillSwitch.checked = state.fillHeight;
|
||||
setTitle(fillSwitchRow, state.fillHeight
|
||||
? "Back to the automatic height; the prompt area stops following the node"
|
||||
: "Let the prompt area grow with the node instead of being capped");
|
||||
panel.classList.toggle("bpi-fill", state.fillHeight);
|
||||
fillSwitch.addEventListener("change", () => setFillHeight(fillSwitch.checked));
|
||||
bindMirrorAction(sortPromptButton, () => {
|
||||
if (state.animaSortSnapshot) cancelAnimaSort();
|
||||
else sortByAnimaOrder();
|
||||
@@ -3638,6 +3754,9 @@ function createPanel(node, textWidget) {
|
||||
});
|
||||
modeSelect.addEventListener("change", () => {
|
||||
state.modePreference = modeSelect.value;
|
||||
if (!node.properties || typeof node.properties !== "object") node.properties = {};
|
||||
if (state.modePreference === "auto") delete node.properties.bpiMode;
|
||||
else node.properties.bpiMode = state.modePreference;
|
||||
state.editing = null;
|
||||
render();
|
||||
});
|
||||
@@ -3730,12 +3849,20 @@ function createPanel(node, textWidget) {
|
||||
return state.collapsedWidgetHeight;
|
||||
},
|
||||
getMaxHeight() {
|
||||
return state.collapsedWidgetHeight;
|
||||
// 适应高度模式把上限抬高:两者相等时 ComfyUI 只按下限给,拉大节点多出来的空间全变空白
|
||||
return state.fillHeight ? EXPANDED_WIDGET_MAX_HEIGHT : state.collapsedWidgetHeight;
|
||||
},
|
||||
refreshAfterConfigure() {
|
||||
// Workflow files keep the hidden-tag list in node.properties; make sure
|
||||
// the bar is drawn even when the text itself did not change.
|
||||
getHiddenTags();
|
||||
// 工作流里的模式偏好要先回到下拉,否则按钮显隐会退回「自动」那一套
|
||||
state.modePreference = storedMode();
|
||||
modeSelect.value = state.modePreference;
|
||||
// 工作流里的「适应高度」也要还原回按钮和面板 class
|
||||
state.fillHeight = storedFillHeight();
|
||||
panel.classList.toggle("bpi-fill", state.fillHeight);
|
||||
fillSwitch.checked = state.fillHeight;
|
||||
scheduleRender(true);
|
||||
},
|
||||
syncSourceBar() {
|
||||
|
||||
+2
-2
@@ -1226,7 +1226,7 @@ function injectBpiStyles() {
|
||||
.dark-theme{--bpi-field-bg:#0b0d10;--bpi-field-border:#333a45;--bpi-switch-off:#39414f;--bpi-switch-knob:#e8edf5;--bpi-switch-off-border:#4c5668;--bpi-custom-bg:#17342c;--bpi-custom-border:#3f8f77;--bpi-custom-text:#7fdcbd;--bpi-surface:rgba(10,12,18,.72);--bpi-surface-2:#171b22;--bpi-surface-3:#14181f;--bpi-surface-table:#11151b;--bpi-surface-modal:#20252d;--bpi-text:#e6edf7;--bpi-text-muted:#aab5c5;--bpi-text-faint:#7f8a9a;--bpi-border:#3a4250;--bpi-border-2:#4c5668;--bpi-border-3:#29313d;--bpi-button-bg:#2c3340;--bpi-button-text:#e8edf5;--bpi-input-bg:#171b22;--bpi-input-text:#eef3fb;--bpi-head-bg:#252c36;--bpi-danger-bg:#65313b;--bpi-danger-bg-hover:#7d3d49;--bpi-danger-border:#8d4653;--bpi-danger-text:#ffd9de;--bpi-break-bg:#17342c;--bpi-break-bg-hover:#0f6e56;--bpi-break-border:#3f8f77;--bpi-break-text:#7fdcbd;--bpi-primary-bg:#285f8e;--bpi-primary-bg-hover:#346f9f;--bpi-primary-border:#3f83ba;--bpi-primary-text:#eaf3fb;--bpi-en-head-text:#cbe8ff;--bpi-en-hint-text:#91b4cf;--bpi-zh-head-text:#cbe9c8;--bpi-zh-hint-text:#91ad93;--bpi-cat-name-text:#9ee8a7;--bpi-info-text:#87bfea;--bpi-link-text:#76c9ff;--bpi-zh-text:#9ed0ff;--bpi-status-error:#ff8b93;--bpi-status-ok:#7fdda2;--bpi-status-busy:#ffd27a;--bpi-badge-warn:#e2ca78;--bpi-badge-source:#86d5c4;--bpi-badge-suspected:#7fc4e0;--bpi-badge-machine:#cbb1ff;--bpi-star-on:#ffd45f;--bpi-star-off:#8f9aaa;--bpi-quick-head-text:#d9ccff;--bpi-quick-hint-text:#a795c6;--bpi-handle-text:#7194ad;--bpi-handle-hover:#a9c8e4;--bpi-mini-hover-bg:#3c4b60;--bpi-handle-hover-bg:#2b3a4d;--bpi-chip-hide-bg:#1d2937;--bpi-waiting-bg:rgba(70,52,25,.5);--bpi-waiting-border:#8a5f16;--bpi-hidden-chip-hover-bg:#31404f;--bpi-issues-bg:rgba(55,39,25,.55);--bpi-issues-border:#5b4a36;--bpi-issue-text:#e6c792;--bpi-row-hover-overlay:rgba(76,167,232,.34);--bpi-row-pinned-overlay:#314b68;--bpi-row-hover-ring:rgba(76,167,232,.7);--bpi-row-pinned-ring:#76c9ff;--bpi-row-hover-text:#fff;--bpi-row-hover-shadow:0 0 7px rgba(109,190,255,.6);--bpi-row-selected-bg:#314b68;--bpi-en-section-bg:rgba(76,167,232,.08);--bpi-quick-section-bg:rgba(167,149,198,.08);--bpi-dup-overlay:rgba(255,139,147,.22);--bpi-editor-bg:#111923;--bpi-hit-bg:#5a4413;--bpi-hit-text:#ffe6a8}
|
||||
[data-bpi-theme="dark"]{--bpi-field-bg:#0b0d10;--bpi-field-border:#333a45;--bpi-switch-off:#39414f;--bpi-switch-knob:#e8edf5;--bpi-switch-off-border:#4c5668;--bpi-custom-bg:#17342c;--bpi-custom-border:#3f8f77;--bpi-custom-text:#7fdcbd;--bpi-surface:rgba(10,12,18,.72);--bpi-surface-2:#171b22;--bpi-surface-3:#14181f;--bpi-surface-table:#11151b;--bpi-surface-modal:#20252d;--bpi-text:#e6edf7;--bpi-text-muted:#aab5c5;--bpi-text-faint:#7f8a9a;--bpi-border:#3a4250;--bpi-border-2:#4c5668;--bpi-border-3:#29313d;--bpi-button-bg:#2c3340;--bpi-button-text:#e8edf5;--bpi-input-bg:#171b22;--bpi-input-text:#eef3fb;--bpi-head-bg:#252c36;--bpi-danger-bg:#65313b;--bpi-danger-bg-hover:#7d3d49;--bpi-danger-border:#8d4653;--bpi-danger-text:#ffd9de;--bpi-break-bg:#17342c;--bpi-break-bg-hover:#0f6e56;--bpi-break-border:#3f8f77;--bpi-break-text:#7fdcbd;--bpi-primary-bg:#285f8e;--bpi-primary-bg-hover:#346f9f;--bpi-primary-border:#3f83ba;--bpi-primary-text:#eaf3fb;--bpi-en-head-text:#cbe8ff;--bpi-en-hint-text:#91b4cf;--bpi-zh-head-text:#cbe9c8;--bpi-zh-hint-text:#91ad93;--bpi-cat-name-text:#9ee8a7;--bpi-info-text:#87bfea;--bpi-link-text:#76c9ff;--bpi-zh-text:#9ed0ff;--bpi-status-error:#ff8b93;--bpi-status-ok:#7fdda2;--bpi-status-busy:#ffd27a;--bpi-badge-warn:#e2ca78;--bpi-badge-source:#86d5c4;--bpi-badge-suspected:#7fc4e0;--bpi-badge-machine:#cbb1ff;--bpi-star-on:#ffd45f;--bpi-star-off:#8f9aaa;--bpi-quick-head-text:#d9ccff;--bpi-quick-hint-text:#a795c6;--bpi-handle-text:#7194ad;--bpi-handle-hover:#a9c8e4;--bpi-mini-hover-bg:#3c4b60;--bpi-handle-hover-bg:#2b3a4d;--bpi-chip-hide-bg:#1d2937;--bpi-waiting-bg:rgba(70,52,25,.5);--bpi-waiting-border:#8a5f16;--bpi-hidden-chip-hover-bg:#31404f;--bpi-issues-bg:rgba(55,39,25,.55);--bpi-issues-border:#5b4a36;--bpi-issue-text:#e6c792;--bpi-row-hover-overlay:rgba(76,167,232,.34);--bpi-row-pinned-overlay:#314b68;--bpi-row-hover-ring:rgba(76,167,232,.7);--bpi-row-pinned-ring:#76c9ff;--bpi-row-hover-text:#fff;--bpi-row-hover-shadow:0 0 7px rgba(109,190,255,.6);--bpi-row-selected-bg:#314b68;--bpi-en-section-bg:rgba(76,167,232,.08);--bpi-quick-section-bg:rgba(167,149,198,.08);--bpi-dup-overlay:rgba(255,139,147,.22);--bpi-editor-bg:#111923;--bpi-hit-bg:#5a4413;--bpi-hit-text:#ffe6a8}
|
||||
[data-bpi-theme="light"]{--bpi-field-bg:#fff;--bpi-field-border:#bbb;--bpi-switch-off:#c9ced6;--bpi-switch-knob:#fff;--bpi-switch-off-border:#b0b6bf;--bpi-custom-bg:#e6f6f1;--bpi-custom-border:#7fc9b0;--bpi-custom-text:#0f6e56;--bpi-surface:rgba(255,255,255,.92);--bpi-surface-2:#f5f5f5;--bpi-surface-3:#eaeaea;--bpi-surface-table:#f0f0f0;--bpi-surface-modal:#fff;--bpi-text:#333;--bpi-text-muted:#666;--bpi-text-faint:#999;--bpi-border:#ccc;--bpi-border-2:#bbb;--bpi-border-3:#e0e0e0;--bpi-button-bg:#e8e8e8;--bpi-button-text:#333;--bpi-input-bg:#fff;--bpi-input-text:#333;--bpi-head-bg:#e8e8e8;--bpi-danger-bg:#c0392b;--bpi-danger-bg-hover:#a53023;--bpi-danger-border:#a53023;--bpi-danger-text:#fff;--bpi-break-bg:#e6f6f1;--bpi-break-bg-hover:#0f6e56;--bpi-break-border:#7fc9b0;--bpi-break-text:#0f6e56;--bpi-primary-bg:#2b6cb0;--bpi-primary-bg-hover:#245a94;--bpi-primary-border:#1e4f85;--bpi-primary-text:#fff;--bpi-en-head-text:#17557f;--bpi-en-hint-text:#5b7f9e;--bpi-zh-head-text:#1f5c22;--bpi-zh-hint-text:#5b7d5e;--bpi-cat-name-text:#1e5a28;--bpi-info-text:#1f5f96;--bpi-link-text:#1a5f96;--bpi-zh-text:#18508a;--bpi-status-error:#b3261e;--bpi-status-ok:#1a7f45;--bpi-status-busy:#8a5a00;--bpi-badge-warn:#8a5a00;--bpi-badge-source:#0f6e56;--bpi-badge-suspected:#1c5f80;--bpi-badge-machine:#5b3fa8;--bpi-star-on:#a8750a;--bpi-star-off:#6b7280;--bpi-quick-head-text:#4a3a7a;--bpi-quick-hint-text:#6b5a8f;--bpi-handle-text:#4a6a85;--bpi-handle-hover:#1f5f96;--bpi-mini-hover-bg:#d8dee7;--bpi-handle-hover-bg:#e2e8ef;--bpi-chip-hide-bg:#eef1f5;--bpi-waiting-bg:#fdf1dc;--bpi-waiting-border:#d9a95a;--bpi-hidden-chip-hover-bg:#e8eef5;--bpi-issues-bg:#fdf6e8;--bpi-issues-border:#d9b98a;--bpi-issue-text:#7a5410;--bpi-row-hover-overlay:rgba(47,108,176,.06);--bpi-row-pinned-overlay:#cfe3f7;--bpi-row-hover-ring:rgba(47,108,176,.55);--bpi-row-pinned-ring:#1a5f96;--bpi-row-hover-text:#1f1f1f;--bpi-row-hover-shadow:none;--bpi-row-selected-bg:#dcebf8;--bpi-en-section-bg:rgba(43,108,176,.06);--bpi-quick-section-bg:rgba(107,85,148,.06);--bpi-dup-overlay:rgba(179,38,30,.12);--bpi-editor-bg:#fff;--bpi-hit-bg:#fbe49c;--bpi-hit-text:#7a5410}
|
||||
.bpi-panel{box-sizing:border-box;width:100%;height:100%;min-height:var(--bpi-collapsed-height,390px);max-height:var(--bpi-collapsed-height,390px);padding:8px;display:flex;flex-direction:column;gap:7px;color:var(--bpi-text);font:12px/1.4 Arial,sans-serif;background:var(--bpi-surface);border:1px solid var(--bpi-border);border-radius:8px;overflow-y:auto;scrollbar-gutter:stable}
|
||||
.bpi-panel{box-sizing:border-box;width:100%;height:100%;min-height:var(--bpi-collapsed-height,390px);max-height:var(--bpi-collapsed-height,390px);padding:8px;display:flex;flex-direction:column;gap:7px;color:var(--bpi-text);font:12px/1.4 Arial,sans-serif;background:var(--bpi-surface);border:1px solid var(--bpi-border);border-radius:8px;overflow-y:auto;scrollbar-gutter:stable}.bpi-panel.bpi-fill{max-height:none}.bpi-panel.bpi-fill > .bpi-english-section{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}.bpi-panel.bpi-fill > .bpi-english-section > *{flex:none}.bpi-panel.bpi-fill > .bpi-english-section > .bpi-english-token-view{flex:1 1 auto;min-height:92px;max-height:none}.bpi-panel.bpi-fill > .bpi-english-section > .bpi-english-editor{flex:1 1 auto;min-height:120px}
|
||||
.bpi-english-section{flex:none;border:1px solid #3e7197;border-radius:7px;background-color:var(--bpi-en-section-bg);overflow:hidden}.bpi-english-head{display:flex;align-items:center;gap:7px;padding:5px 8px;background:var(--bpi-head-bg);color:var(--bpi-en-head-text);font-weight:700;flex-wrap:wrap}.bpi-english-hint{margin-left:auto;color:var(--bpi-en-hint-text);font-size:10px;font-weight:400}.bpi-english-token-view{box-sizing:border-box;width:100%;height:auto;min-height:92px;max-height:360px;overflow:auto;scrollbar-gutter:stable;padding:8px 9px;outline:none;color:var(--bpi-text);font:12px/1.85 Consolas,monospace;white-space:normal}.bpi-english-token-view:focus{box-shadow:inset 0 0 0 1px #5aa6d8}.bpi-english-editor{box-sizing:border-box;width:100%;height:120px;min-height:92px;max-height:420px;overflow-y:auto;resize:vertical;border:0;border-top:1px solid #3e7197;background-color:transparent;color:var(--bpi-text);padding:9px;outline:none;font:12px/1.55 Consolas,monospace}.bpi-english-editor:focus{box-shadow:inset 0 0 0 1px #5aa6d8}.bpi-english-token{font-family:Consolas,monospace;color:var(--bpi-text)}.bpi-english-token.bpi-linked{background:#147fc3;border-color:#76c9ff;color:#fff;box-shadow:0 0 0 1px rgba(118,201,255,.32)}
|
||||
.bpi-mirror-section{flex:none;border:1px solid #488739;border-radius:7px;background:var(--bpi-surface);overflow:visible}.bpi-section-head{display:flex;align-items:center;gap:7px;padding:5px 8px;background:var(--bpi-head-bg);color:var(--bpi-zh-head-text);font-weight:700;flex-wrap:wrap}.bpi-section-hint{margin-left:auto;color:var(--bpi-zh-hint-text);font-size:10px;font-weight:400}.bpi-mirror-actions{display:flex;gap:4px;align-items:center;flex-wrap:wrap}.bpi-chinese-mirror{box-sizing:border-box;width:100%;height:auto;min-height:92px;max-height:360px;overflow:auto;resize:none;scrollbar-gutter:stable;padding:8px 9px;outline:none;color:var(--bpi-text);line-height:1.85;cursor:text;white-space:normal}.bpi-chinese-mirror:focus{box-shadow:inset 0 0 0 1px #70b35f}.bpi-category-table{display:grid;grid-template-columns:minmax(110px,150px) minmax(0,1fr);border:1px solid #315f3a;border-radius:6px;overflow:hidden;background:var(--bpi-surface-3)}.bpi-category-row{display:contents}.bpi-category-name,.bpi-category-content{padding:6px 8px;border-top:1px solid #31513a}.bpi-category-row:first-child .bpi-category-name,.bpi-category-row:first-child .bpi-category-content{border-top:0}.bpi-category-name{background:var(--bpi-head-bg);border-right:1px solid #31513a;color:var(--bpi-cat-name-text);font-weight:700}.bpi-category-content{min-width:0}.bpi-chinese-editor{box-sizing:border-box;width:100%;height:auto;min-height:110px;max-height:600px;overflow-y:auto;resize:vertical;border:0;border-top:1px solid #3b693c;background:var(--bpi-editor-bg);color:var(--bpi-text);padding:9px;outline:none;font:12px/1.65 Arial,sans-serif}.bpi-chinese-editor:focus{box-shadow:inset 0 0 0 1px #70b35f}.bpi-hidden{display:none!important}.bpi-mirror-empty{color:var(--bpi-text-muted)}.bpi-mirror-token{display:inline-flex;align-items:center;border:1px solid transparent;border-radius:5px;padding:0 3px;margin:1px 0;cursor:pointer;transition:background .12s,border-color .12s,color .12s}.bpi-mirror-token:hover{background:#345a3b;border-color:#5c8d63;color:#fff}.bpi-mirror-token.bpi-linked{background:#315f83;border-color:#76a9ff;color:#fff}
|
||||
.bpi-mirror-token.bpi-linked,.bpi-english-token.bpi-linked{background:#147fc3;border-color:#76c9ff;color:#fff;box-shadow:0 0 0 1px rgba(118,201,255,.32)}.bpi-english-token-view .bpi-mirror-separator{color:#7194ad}
|
||||
@@ -1425,7 +1425,7 @@ function injectBpiStyles() {
|
||||
.bpi-switch-text{font-size:12px;color:var(--bpi-text-muted)}
|
||||
.bpi-switch-row input:checked ~ .bpi-switch-text{color:var(--bpi-status-error)}
|
||||
.bpi-switch-hint{display:none;font-size:11px;color:var(--bpi-status-error)}
|
||||
.bpi-switch-row input:checked ~ .bpi-switch-hint{display:inline}
|
||||
.bpi-switch-row input:checked ~ .bpi-switch-hint{display:inline}.bpi-switch-row.bpi-switch-accent input:checked + .bpi-switch-track{background-color:var(--bpm-accent);border-color:var(--bpm-accent-border)}.bpi-switch-row.bpi-switch-accent input:checked ~ .bpi-switch-text{color:var(--bpi-text)}
|
||||
/* 设置面板里的输入框 / 下拉:直接用 ComfyUI 的字段底色与描边,取不到才用兜底值,
|
||||
免得暗色下我们的蓝灰底比面板亮一截、显得突兀 */
|
||||
.bpi-settings-form .bpi-form input,.bpi-settings-form .bpi-form select,.bpi-settings-form .bpi-form textarea{background-color:var(--p-textfield-background,var(--p-inputtext-background,var(--bpi-field-bg)));border-color:var(--p-textfield-border-color,var(--p-inputtext-border-color,var(--bpi-field-border)))}
|
||||
|
||||
@@ -13,7 +13,9 @@ function injectStyles() {
|
||||
const style = document.createElement("style");
|
||||
style.id = STYLE_ID;
|
||||
style.textContent = [
|
||||
".bpi-dup-pill{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:999px;font-size:12px;line-height:1.5;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
||||
// holder 才是交给画布的 widget 元素:节点拖大时它被拉伸,胶囊自己锁死高度不跟着变
|
||||
`.bpi-dup-holder{display:flex;align-items:center;justify-content:flex-start;width:100%;min-height:${PILL_HEIGHT}px;overflow:hidden}`,
|
||||
`.bpi-dup-pill{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:999px;font-size:12px;line-height:1.5;height:${PILL_HEIGHT}px;box-sizing:border-box;flex:none;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}`,
|
||||
".bpi-dup-pill .bpi-dup-dot{width:7px;height:7px;border-radius:50%;flex:none}",
|
||||
".bpi-dup-pill .bpi-dup-text{overflow:hidden;text-overflow:ellipsis}",
|
||||
".bpi-dup-pill.bpi-dup-warn{background:rgba(214,158,46,.18);color:#8a5a00}",
|
||||
@@ -117,6 +119,8 @@ app.registerExtension({
|
||||
const toggle = this.widgets?.find((widget) => widget.name === "keep_first");
|
||||
if (toggle) toggle.label = "只保留第一个";
|
||||
if (this.inputs?.[0]) this.inputs[0].label = "提示词";
|
||||
const holder = document.createElement("div");
|
||||
holder.className = "bpi-dup-holder";
|
||||
const pill = document.createElement("div");
|
||||
pill.className = "bpi-dup-pill bpi-dup-idle";
|
||||
const dot = document.createElement("span");
|
||||
@@ -124,9 +128,10 @@ app.registerExtension({
|
||||
const textNode = document.createElement("span");
|
||||
textNode.className = "bpi-dup-text";
|
||||
pill.append(dot, textNode);
|
||||
holder.append(pill);
|
||||
this._duplicateCheck = { pill, dot, textNode, count: 0, connected: false };
|
||||
try {
|
||||
this.addDOMWidget("bpi_duplicate_status", "bpi-duplicate-status", pill, {
|
||||
this.addDOMWidget("bpi_duplicate_status", "bpi-duplicate-status", holder, {
|
||||
getMinHeight: () => PILL_HEIGHT,
|
||||
getMaxHeight: () => PILL_HEIGHT,
|
||||
hideOnZoom: false,
|
||||
|
||||
+11
-4
@@ -103,7 +103,10 @@ const ZH = {
|
||||
"Highest": "最高",
|
||||
Translate: "翻译",
|
||||
"Translate & optimize": "翻译并优化",
|
||||
"Optimize to Anima": "优化为 Anima",
|
||||
Optimize: "优化",
|
||||
"Fit height": "适应高度",
|
||||
"Let the prompt area grow with the node instead of being capped": "让提示词区跟着节点一起变高,不再卡在固定的高度上限",
|
||||
"Back to the automatic height; the prompt area stops following the node": "回到自动高度,提示词区不再跟着节点变高",
|
||||
"Re-translate": "重新翻译",
|
||||
"Translate all unknown": "翻译全部未收录",
|
||||
"Sort by Anima order": "按 Anima 顺序排序",
|
||||
@@ -403,8 +406,8 @@ const ZH = {
|
||||
"Translation Service": "翻译服务",
|
||||
"Dictionary only (no API)": "仅词库(不需要接口)",
|
||||
"Baidu Translate": "百度翻译",
|
||||
"Only this service's settings are shown below. \"Translate & Optimize\" and \"Optimize to Anima\" always use AI: choose OpenAI-compatible API or Ollama above to configure or test them. Switching service never clears saved keys.":
|
||||
"下面只显示当前所选服务的设置。「翻译并优化」和「优化为 Anima」始终走 AI:要配置或测试它们,请在上方选择「OpenAI 兼容接口」或「Ollama」。切换服务不会清掉已保存的密钥。",
|
||||
"Only this service's settings are shown below. \"Translate & Optimize\" and \"Optimize\" always use AI: choose OpenAI-compatible API or Ollama above to configure or test them. Switching service never clears saved keys.":
|
||||
"下面只显示当前所选服务的设置。「翻译并优化」和「优化」始终走 AI:要配置或测试它们,请在上方选择「OpenAI 兼容接口」或「Ollama」。切换服务不会清掉已保存的密钥。",
|
||||
"APP ID": "APP ID",
|
||||
"Secret Key": "密钥",
|
||||
"Clear saved Baidu secret": "清除已保存的百度密钥",
|
||||
@@ -621,6 +624,9 @@ const ZH = {
|
||||
"Translation Rule": "翻译规则",
|
||||
"Translate & Optimize Rule": "翻译并优化规则",
|
||||
"Optimize Rule": "优化规则",
|
||||
"Natural Language Rule": "自然语言规则",
|
||||
"Optimize with the rule for the current mode: tag mode tidies the tag flow, natural language mode rewrites it as English sentences":
|
||||
"按当前模式对应的规则优化:标签模式整理标签流,自然语言模式改写为英文自然语句",
|
||||
"Connection OK: {}": "连接正常:{}",
|
||||
// 确认对话框的默认确认按钮(confirmDialog 的 confirmLabel 缺省值),
|
||||
// 不是「连接正常」的 OK——那个是整句键 "Connection OK: {}"。
|
||||
@@ -795,7 +801,8 @@ const ZH = {
|
||||
|
||||
"Click a tag to link it with the detail table below; select then press Delete": "点击标签可与下方明细表联动;选中后按 Delete 删除",
|
||||
"English prompt is empty; nothing to optimize": "英文提示词是空的,没有可优化的内容",
|
||||
"“Optimize to Anima” only processes English; translate Chinese in the sidebar first": "「优化为 Anima」只处理英文;中文请先在侧边栏翻译",
|
||||
"“Optimize” only processes English in tag mode; translate Chinese in the sidebar first": "「优化」在标签模式下只处理英文;中文请先在侧边栏翻译,或把模式切到「自然语言」",
|
||||
"(Chinese detected; translating to English first)": "(检测到中文,会先译成英文)",
|
||||
"{} done; please confirm in the preview window": "「{}」完成;请在预览窗口确认",
|
||||
"{}; please fix it in the preview before confirming": "「{}」;请在预览里改好再确认",
|
||||
};
|
||||
|
||||
+7
-2
@@ -692,7 +692,7 @@ async function buildAssistantForm(host, loading) {
|
||||
const error = element("div", "bpi-status");
|
||||
error.style.gridColumn = "1 / -1";
|
||||
|
||||
// 翻译服务:翻译/解释按此选择;“翻译并优化”“优化为 Anima”恒走 AI
|
||||
// 翻译服务:翻译/解释按此选择;“翻译并优化”“优化”恒走 AI
|
||||
const serviceLabel = element("label", "", "Translation Service");
|
||||
const service = element("select");
|
||||
for (const [value, label] of [
|
||||
@@ -708,7 +708,7 @@ async function buildAssistantForm(host, loading) {
|
||||
// 后端已把旧的 "ai" 拆成两个具体服务;这里再兜一次,避免下拉落空
|
||||
service.value = config.translate_service === "ai" ? config.ai_provider || "openai_compatible" : config.translate_service;
|
||||
form.append(serviceLabel, service);
|
||||
const serviceNote = element("div", "bpi-config-note", 'Only this service\'s settings are shown below. "Translate & Optimize" and "Optimize to Anima" always use AI: choose OpenAI-compatible API or Ollama above to configure or test them. Switching service never clears saved keys.');
|
||||
const serviceNote = element("div", "bpi-config-note", 'Only this service\'s settings are shown below. "Translate & Optimize" and "Optimize" always use AI: choose OpenAI-compatible API or Ollama above to configure or test them. Switching service never clears saved keys.');
|
||||
serviceNote.style.gridColumn = "1 / -1";
|
||||
form.append(serviceNote);
|
||||
|
||||
@@ -877,11 +877,13 @@ async function buildAssistantForm(host, loading) {
|
||||
translation: addRule(openaiOnly, "Translation Rule", config.openai_translation_rule),
|
||||
translateOptimize: addRule(openaiOnly, "Translate & Optimize Rule", config.openai_translate_optimize_rule),
|
||||
optimization: addRule(openaiOnly, "Optimize Rule", config.openai_optimization_rule),
|
||||
natural: addRule(openaiOnly, "Natural Language Rule", config.openai_natural_rule),
|
||||
},
|
||||
ollama: {
|
||||
translation: addRule(ollamaOnly, "Translation Rule", config.ollama_translation_rule),
|
||||
translateOptimize: addRule(ollamaOnly, "Translate & Optimize Rule", config.ollama_translate_optimize_rule),
|
||||
optimization: addRule(ollamaOnly, "Optimize Rule", config.ollama_optimization_rule),
|
||||
natural: addRule(ollamaOnly, "Natural Language Rule", config.ollama_natural_rule),
|
||||
},
|
||||
};
|
||||
form.appendChild(error);
|
||||
@@ -909,6 +911,7 @@ async function buildAssistantForm(host, loading) {
|
||||
active.translation.value = config[`default_${prefix}_translation_rule`];
|
||||
active.translateOptimize.value = config[`default_${prefix}_translate_optimize_rule`];
|
||||
active.optimization.value = config[`default_${prefix}_optimization_rule`];
|
||||
active.natural.value = config[`default_${prefix}_natural_rule`];
|
||||
setText(error, "Defaults restored in editor; click Save to apply");
|
||||
error.dataset.kind = "ok";
|
||||
});
|
||||
@@ -965,9 +968,11 @@ async function buildAssistantForm(host, loading) {
|
||||
openai_translation_rule: rules.openai_compatible.translation.value,
|
||||
openai_translate_optimize_rule: rules.openai_compatible.translateOptimize.value,
|
||||
openai_optimization_rule: rules.openai_compatible.optimization.value,
|
||||
openai_natural_rule: rules.openai_compatible.natural.value,
|
||||
ollama_translation_rule: rules.ollama.translation.value,
|
||||
ollama_translate_optimize_rule: rules.ollama.translateOptimize.value,
|
||||
ollama_optimization_rule: rules.ollama.optimization.value,
|
||||
ollama_natural_rule: rules.ollama.natural.value,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "comfyui-bilingual-prompt-inspector",
|
||||
"version": "2.1.5",
|
||||
"version": "2.1.8",
|
||||
"description": "ComfyUI bilingual prompt inspection, translation, Anima sorting, and on-demand Danbooru dictionary extension.",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "prompt-translator-manager"
|
||||
version = "2.1.5"
|
||||
version = "2.1.8"
|
||||
# 与 Comfy Registry 页面上显示的描述保持一致: publish 会用这里的值覆盖网页上的手改
|
||||
description = "ComfyUI prompt node with translation and management: bilingual tag view, drag-to-reorder, temporary hiding, multiple translation services and a sidebar dictionary manager.\nComfyUI 提示节点,支持翻译和管理:双语标签视图、拖拽排序、临时隐藏、多翻译服务和侧边栏词典管理器。"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
@@ -27,6 +27,7 @@ from .assistant_store import (
|
||||
normalize_ai_base_url,
|
||||
openai_chat_endpoint,
|
||||
sanitize_anima_prompt,
|
||||
sanitize_natural_prompt,
|
||||
translation_direction,
|
||||
)
|
||||
from .dictionary_store import DictionaryStore, normalize_key
|
||||
@@ -205,12 +206,14 @@ async def _call_ai_assistant(action, text, instruction, config, connection_test=
|
||||
config.get("ai_provider", "openai_compatible"), "openai"
|
||||
)
|
||||
if not config.get(f"{prefix}_model"):
|
||||
raise ValueError("尚未填写 AI 模型名称(“翻译并优化”“优化为 Anima”以及 AI 翻译都需要)")
|
||||
raise ValueError("尚未填写 AI 模型名称(“翻译并优化”“优化”以及 AI 翻译都需要)")
|
||||
rule = {
|
||||
"translate": config[f"{prefix}_translation_rule"],
|
||||
"explain": config[f"{prefix}_translation_rule"],
|
||||
"translate_optimize": config[f"{prefix}_translate_optimize_rule"],
|
||||
"optimize": config[f"{prefix}_optimization_rule"],
|
||||
# 自然语言模式下的「优化」:输入可以是中文,输出是英文自然语句,不走 Anima 标点规则
|
||||
"optimize_natural": config[f"{prefix}_natural_rule"],
|
||||
}[action]
|
||||
if connection_test:
|
||||
rule = "你是连接测试助手。"
|
||||
@@ -224,8 +227,12 @@ async def _call_ai_assistant(action, text, instruction, config, connection_test=
|
||||
rule = f"{rule}\n\n本次任务:无论输入语言为何,最终必须输出经过优化的英文 Anima 提示词。"
|
||||
elif not connection_test and action == "optimize":
|
||||
if re.search(r"[\u3400-\u9fff]", text):
|
||||
raise ValueError("“优化为 Anima”只接受英文;中文或混合文本请使用“翻译并优化”")
|
||||
raise ValueError("“优化”在标签模式下只接受英文;中文或混合文本请使用“翻译并优化”,或把模式切到“自然语言”")
|
||||
rule = f"{rule}\n\n本次任务:整理现有英文内容,不承担翻译。"
|
||||
elif not connection_test and action == "optimize_natural":
|
||||
# 自然语言模式允许中文输入:先译后整理,输出一段英文自然语句
|
||||
direction = "先把中文忠实译为英文,再整理表达" if translation_direction(text) == "to_english" else "整理现有英文内容,不承担翻译"
|
||||
rule = f"{rule}\n\n本次任务方向:{direction}。"
|
||||
if instruction:
|
||||
rule = f"{rule}\n\n用户本次附加要求优先遵循:{instruction.strip()}"
|
||||
timeout = ClientTimeout(total=config[f"{prefix}_timeout_seconds"])
|
||||
@@ -275,7 +282,9 @@ async def _call_ai_assistant(action, text, instruction, config, connection_test=
|
||||
raise ValueError("模型没有返回文本结果")
|
||||
if len(result) > 50000:
|
||||
raise ValueError("模型返回内容异常过长")
|
||||
if action in {"translate_optimize", "optimize"}:
|
||||
if action == "optimize_natural":
|
||||
result = sanitize_natural_prompt(result)
|
||||
elif action in {"translate_optimize", "optimize"}:
|
||||
result = sanitize_anima_prompt(result)
|
||||
return result
|
||||
|
||||
@@ -283,8 +292,8 @@ async def _call_ai_assistant(action, text, instruction, config, connection_test=
|
||||
async def _call_configured_assistant(action, text, instruction="", connection_test=False):
|
||||
config = assistant_store.config()
|
||||
translate_service = config["translate_service"]
|
||||
# “翻译并优化”“优化为 Anima”恒走 AI;翻译/解释在翻译服务就是某个 AI 后端时也走 AI
|
||||
needs_ai = action in {"translate_optimize", "optimize"} or (
|
||||
# “翻译并优化”“优化”恒走 AI;翻译/解释在翻译服务就是某个 AI 后端时也走 AI
|
||||
needs_ai = action in {"translate_optimize", "optimize", "optimize_natural"} or (
|
||||
action in {"translate", "explain"} and translate_service in AI_SERVICES
|
||||
)
|
||||
if needs_ai:
|
||||
@@ -357,7 +366,7 @@ async def run_assistant(request):
|
||||
action = str(payload.get("action", "translate")) if isinstance(payload, dict) else "translate"
|
||||
text = str(payload.get("text", "")) if isinstance(payload, dict) else ""
|
||||
instruction = str(payload.get("instruction", "")) if isinstance(payload, dict) else ""
|
||||
if action not in {"translate", "explain", "translate_optimize", "optimize"}:
|
||||
if action not in {"translate", "explain", "translate_optimize", "optimize", "optimize_natural"}:
|
||||
raise ValueError("未知的助手操作")
|
||||
if not text.strip():
|
||||
raise ValueError("输入内容为空")
|
||||
|
||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
|
||||
from assistant_store import (
|
||||
AssistantStore,
|
||||
DEFAULT_NATURAL_RULE,
|
||||
DEFAULT_OPTIMIZATION_RULE,
|
||||
DEFAULT_TRANSLATE_OPTIMIZE_RULE,
|
||||
DEFAULT_TRANSLATION_RULE,
|
||||
@@ -18,6 +19,7 @@ from assistant_store import (
|
||||
normalize_ai_base_url,
|
||||
openai_chat_endpoint,
|
||||
sanitize_anima_prompt,
|
||||
sanitize_natural_prompt,
|
||||
translation_direction,
|
||||
)
|
||||
from dictionary_store import DictionaryStore
|
||||
@@ -315,6 +317,47 @@ class AssistantStoreTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "仍包含中文"):
|
||||
sanitize_anima_prompt("masterpiece, 杰作")
|
||||
|
||||
def test_natural_prompt_keeps_sentence_punctuation(self):
|
||||
# 与 Anima 清洗的核心区别:句点是句子的一部分,不能被换成逗号
|
||||
self.assertEqual(
|
||||
sanitize_natural_prompt("A woman stands in the rain. She looks tired."),
|
||||
"A woman stands in the rain. She looks tired.",
|
||||
)
|
||||
# 逗号是句子内部的停顿,原样保留
|
||||
self.assertEqual(
|
||||
sanitize_natural_prompt("A woman stands in the rain, tired and cold."),
|
||||
"A woman stands in the rain, tired and cold.",
|
||||
)
|
||||
# 全角标点统一成半角,代码块围栏和转义符照旧清掉
|
||||
self.assertEqual(
|
||||
sanitize_natural_prompt('```text\nA woman stands in the rain,tired。\n```'),
|
||||
"A woman stands in the rain, tired.",
|
||||
)
|
||||
self.assertEqual(
|
||||
sanitize_natural_prompt('A woman \\"stands\\" in the rain'),
|
||||
'A woman "stands" in the rain',
|
||||
)
|
||||
# 模型爱用列表;自然语言结果里列表符号要去掉,换行收成空格
|
||||
self.assertEqual(
|
||||
sanitize_natural_prompt("1. A woman stands in the rain\n2. She looks tired"),
|
||||
"A woman stands in the rain She looks tired",
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "仍包含中文"):
|
||||
sanitize_natural_prompt("一名女子站在雨中")
|
||||
|
||||
def test_natural_rule_is_persisted_per_backend(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
store = AssistantStore(config_dir=root, install_id_path=Path(root) / "install")
|
||||
store.update({"openai_natural_rule": "我的自然语言规则", "ollama_natural_rule": "本地规则"})
|
||||
config = store.config()
|
||||
self.assertEqual(config["openai_natural_rule"], "我的自然语言规则")
|
||||
self.assertEqual(config["ollama_natural_rule"], "本地规则")
|
||||
# 另一套不受影响,且默认值能通过 public_config 取回
|
||||
self.assertEqual(config["openai_optimization_rule"], DEFAULT_OPTIMIZATION_RULE)
|
||||
public = store.public_config()
|
||||
self.assertEqual(public["default_openai_natural_rule"], DEFAULT_NATURAL_RULE)
|
||||
self.assertEqual(public["default_ollama_natural_rule"], DEFAULT_NATURAL_RULE)
|
||||
|
||||
def test_legacy_default_rules_are_upgraded(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
store = AssistantStore(Path(root) / "config")
|
||||
|
||||
+9
-1
@@ -85,7 +85,15 @@ assert.equal(
|
||||
// 自由文本变量必须用「"…"」包起来才能折叠成 {},否则整句永远匹配不上。
|
||||
// 这里锁死「运行时真实形态」,防止以后有人把引号去掉或换成 ASCII 直引号。
|
||||
for (const [source, expected] of [
|
||||
["“Optimize to Anima” done; please confirm in the preview window", "「优化为 Anima」完成;请在预览窗口确认"],
|
||||
["“Optimize” done; please confirm in the preview window", "「优化」完成;请在预览窗口确认"],
|
||||
["Optimize", "优化"],
|
||||
["Fit height", "适应高度"],
|
||||
["Natural Language Rule", "自然语言规则"],
|
||||
["(Chinese detected; translating to English first)", "(检测到中文,会先译成英文)"],
|
||||
[
|
||||
"Optimize with the rule for the current mode: tag mode tidies the tag flow, natural language mode rewrites it as English sentences",
|
||||
"按当前模式对应的规则优化:标签模式整理标签流,自然语言模式改写为英文自然语句",
|
||||
],
|
||||
["applied “Translate & optimize”; press Ctrl+Z to undo", "已应用 翻译并优化;按 Ctrl+Z 可撤销"],
|
||||
["Undone: “Sort to Anima order”", "已撤销:Sort to Anima order"],
|
||||
["Redone: “Sort to Anima order”", "已重做:Sort to Anima order"],
|
||||
|
||||
Reference in New Issue
Block a user