IdlecloudX commited on
Commit
b073a7b
·
verified ·
1 Parent(s): 20f1d29

Upload 3 files

Browse files
Files changed (2) hide show
  1. app.py +191 -104
  2. translator.py +118 -99
app.py CHANGED
@@ -1,4 +1,5 @@
1
  import os
 
2
  import gradio as gr
3
  import huggingface_hub
4
  import numpy as np
@@ -7,7 +8,6 @@ import pandas as pd
7
  from PIL import Image
8
  from huggingface_hub import login
9
 
10
- # 导入修改后的翻译函数
11
  from translator import translate_texts
12
 
13
  # ------------------------------------------------------------------
@@ -19,10 +19,7 @@ LABEL_FILENAME = "selected_tags.csv"
19
 
20
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
21
  if HF_TOKEN:
22
- try:
23
- login(token=HF_TOKEN)
24
- except Exception as e:
25
- print(f"Hugging Face登录失败: {e}")
26
  else:
27
  print("⚠️ 未检测到 HF_TOKEN,私有模型可能下载失败")
28
 
@@ -125,38 +122,114 @@ except RuntimeError as e:
125
  # Gradio UI
126
  # ------------------------------------------------------------------
127
  custom_css = """
128
- .label-container { max-height: 300px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; border-radius: 5px; background-color: #f9f9f9; }
129
- .tag-item { display: flex; justify-content: space-between; align-items: center; margin: 2px 0; padding: 2px 5px; border-radius: 3px; background-color: #fff; transition: background-color 0.2s; }
130
- .tag-item:hover { background-color: #f0f0f0; }
131
- .tag-en { font-weight: bold; color: #333; cursor: pointer; }
132
- .tag-zh { color: #666; margin-left: 10px; }
133
- .tag-score { color: #999; font-size: 0.9em; }
134
- .btn-analyze-container { margin-top: 15px; margin-bottom: 15px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  """
136
 
137
  _js_functions = """
138
  function copyToClipboard(text) {
 
 
 
 
 
 
 
139
  if (typeof text === 'undefined' || text === null) {
140
- console.warn('copyToClipboard was called with undefined or null text.');
 
141
  return;
142
  }
 
143
  navigator.clipboard.writeText(text).then(() => {
 
144
  const feedback = document.createElement('div');
145
- let displayText = String(text);
 
 
146
  displayText = displayText.substring(0, 30) + (displayText.length > 30 ? '...' : '');
 
147
  feedback.textContent = '已复制: ' + displayText;
148
- Object.assign(feedback.style, {
149
- position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)',
150
- backgroundColor: '#4CAF50', color: 'white', padding: '10px 20px',
151
- borderRadius: '5px', zIndex: '10000', transition: 'opacity 0.5s ease-out'
152
- });
 
 
 
 
 
153
  document.body.appendChild(feedback);
154
  setTimeout(() => {
155
  feedback.style.opacity = '0';
156
- setTimeout(() => { if (document.body.contains(feedback)) document.body.removeChild(feedback); }, 500);
 
 
 
 
157
  }, 1500);
158
  }).catch(err => {
159
- console.error('Failed to copy tag. Error:', err, 'Text:', text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  });
161
  }
162
  """
@@ -180,20 +253,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="AI 图像标签分析器", css=cus
180
  gen_slider = gr.Slider(0, 1, value=0.35, step=0.01, label="通用标签阈值", info="越高 → 标签更少更准")
181
  char_slider = gr.Slider(0, 1, value=0.85, step=0.01, label="角色标签阈值", info="推荐保持较高阈值")
182
  show_tag_scores = gr.Checkbox(True, label="在列表中显示标签置信度")
183
-
184
- with gr.Accordion("🔑 翻译服务配置", open=False):
185
- enable_translation_cb = gr.Checkbox(label="启用翻译", value=True, info="取消勾选则不进行翻译")
186
- gr.Markdown("提供 **系统访问密钥** 或 **自定义API密钥** 来启用翻译功能。如果两者均未提供或不正确,将不进行翻译。")
187
-
188
- with gr.Tabs():
189
- with gr.TabItem("使用系统密钥"):
190
- system_key_input = gr.Textbox(label="系统访问密钥", type="password", placeholder="输入管理员提供的密钥")
191
- with gr.TabItem("使用自定义API"):
192
- gr.Markdown("在此处填入你自己的翻译API密钥。")
193
- tencent_id_input = gr.Textbox(label="腾讯云 SecretId", type="password")
194
- tencent_key_input = gr.Textbox(label="腾讯云 SecretKey", type="password")
195
- baidu_json_input = gr.Textbox(label="百度翻译凭证 (JSON格式)", type="password", placeholder='[{"app_id":"...", "secret_key":"..."}]')
196
-
197
  with gr.Accordion("📊 标签汇总设置", open=True):
198
  gr.Markdown("选择要包含在下方汇总文本框中的标签类别:")
199
  with gr.Row():
@@ -223,24 +283,27 @@ with gr.Blocks(theme=gr.themes.Soft(), title="AI 图像标签分析器", css=cus
223
  show_copy_button=True
224
  )
225
 
226
- def format_tags_html(tags_dict, translations_list, show_scores=True, show_translation_in_list=True):
 
227
  if not tags_dict:
228
  return "<p>暂无标签</p>"
229
 
230
  html = '<div class="label-container">'
 
 
 
 
231
  tag_keys = list(tags_dict.keys())
232
 
233
  for i, tag in enumerate(tag_keys):
234
  score = tags_dict[tag]
235
- escaped_tag = tag.replace("'", "\\'")
236
 
237
  html += '<div class="tag-item">'
238
  tag_display_html = f'<span class="tag-en" onclick="copyToClipboard(\'{escaped_tag}\')">{tag}</span>'
239
 
240
- translation_text = translations_list[i] if i < len(translations_list) else None
241
- # 仅当翻译文本存在且与原文不同时显示
242
- if show_translation_in_list and translation_text and translation_text != tag:
243
- tag_display_html += f'<span class="tag-zh">({translation_text})</span>'
244
 
245
  html += f'<div>{tag_display_html}</div>'
246
  if show_scores:
@@ -253,17 +316,20 @@ with gr.Blocks(theme=gr.themes.Soft(), title="AI 图像标签分析器", css=cus
253
  current_res, current_translations_dict,
254
  s_gen, s_char, s_rat, s_sep_type, s_show_zh
255
  ):
256
- if not current_res: return "请先分析图像或选择要汇总的标签类别。"
 
257
 
258
  summary_parts = []
259
- separator = {"逗号": ", ", "换行": "\n", "空格": " "}.get(s_sep_type, ", ")
 
260
 
261
  categories_to_summarize = []
262
  if s_gen: categories_to_summarize.append("general")
263
  if s_char: categories_to_summarize.append("characters")
264
  if s_rat: categories_to_summarize.append("ratings")
265
 
266
- if not categories_to_summarize: return "请至少选择一个标签类别进行汇总。"
 
267
 
268
  for cat_key in categories_to_summarize:
269
  if current_res.get(cat_key):
@@ -272,85 +338,97 @@ with gr.Blocks(theme=gr.themes.Soft(), title="AI 图像标签分析器", css=cus
272
  cat_translations = current_translations_dict.get(cat_key, [])
273
 
274
  for i, en_tag in enumerate(cat_tags_en):
275
- translation_text = cat_translations[i] if i < len(cat_translations) else None
276
- # 仅当勾选显示中文、翻译文本存在且与原文不同时,才加入翻译
277
- if s_show_zh and translation_text and translation_text != en_tag:
278
- tags_to_join.append(f"{en_tag}({translation_text})")
279
  else:
280
  tags_to_join.append(en_tag)
281
- if tags_to_join:
282
  summary_parts.append(separator.join(tags_to_join))
283
 
284
- joiner = "\n\n" if separator != "\n" and len(summary_parts) > 1 else separator
 
 
285
  final_summary = joiner.join(summary_parts)
286
  return final_summary if final_summary else "选定的类别中没有找到标签。"
287
 
 
 
288
  def process_image_and_generate_outputs(
289
  img, g_th, c_th, s_scores, # Main inputs
290
- s_gen, s_char, s_rat, s_sep, s_zh_in_sum, # Summary controls
291
- # New translation controls
292
- enable_translation, sys_key, tc_id, tc_key, baidu_json
293
  ):
294
- initial_yield_state = (
295
- gr.update(interactive=True, value="🚀 开始分析"), # btn
296
- "", "", "", "", # html outputs
297
- gr.update(placeholder="分析失败..."), # summary
298
- {}, {}, {} # states
299
- )
300
  if img is None:
301
- yield (gr.update(visible=True, value="❌ 请先上传图片。"), *initial_yield_state)
 
 
 
 
 
 
302
  return
303
 
304
  if tagger_instance is None:
305
- yield (gr.update(visible=True, value="❌ 分析器未成功初始化,请检查控制台错误。"), *initial_yield_state)
 
 
 
 
 
 
306
  return
307
 
308
  yield (
309
  gr.update(interactive=False, value="🔄 处理中..."),
310
  gr.update(visible=True, value="🔄 正在分析图像,请稍候..."),
311
- gr.HTML(value="<p>分析中...</p>"), gr.HTML(value="<p>分析中...</p>"), gr.HTML(value="<p>分析中...</p>"),
312
- gr.update(value="分析中,请稍候..."), {}, {}, {}
 
 
 
313
  )
314
 
315
  try:
 
316
  res, tag_categories_original_order = tagger_instance.predict(img, g_th, c_th)
 
 
 
 
 
 
 
 
317
 
318
  current_translations_dict = {}
319
- if enable_translation:
320
- all_tags_to_translate = []
321
- for cat_key in ["general", "characters", "ratings"]:
322
- all_tags_to_translate.extend(tag_categories_original_order.get(cat_key, []))
323
-
324
- all_translations_flat = []
325
- if all_tags_to_translate:
326
- # 使用新的参数调用翻译函数
327
- all_translations_flat = translate_texts(
328
- texts=all_tags_to_translate,
329
- system_key_input=sys_key,
330
- tencent_id=tc_id,
331
- tencent_key=tc_key,
332
- baidu_creds_json_str=baidu_json
333
- )
334
-
335
- offset = 0
336
- for cat_key in ["general", "characters", "ratings"]:
337
- num_tags_in_cat = len(tag_categories_original_order.get(cat_key, []))
338
- current_translations_dict[cat_key] = all_translations_flat[offset : offset + num_tags_in_cat] if num_tags_in_cat > 0 else []
339
  offset += num_tags_in_cat
340
- else: # 如果未启用翻译,则用空列表填充
341
- for cat_key in ["general", "characters", "ratings"]:
342
- current_translations_dict[cat_key] = []
343
 
344
- general_html = format_tags_html(res.get("general", {}), current_translations_dict.get("general", []), s_scores, enable_translation)
345
- char_html = format_tags_html(res.get("characters", {}), current_translations_dict.get("characters", []), s_scores, enable_translation)
346
- rating_html = format_tags_html(res.get("ratings", {}), current_translations_dict.get("ratings", []), s_scores, enable_translation)
347
 
348
- summary_text = generate_summary_text_content(res, current_translations_dict, s_gen, s_char, s_rat, s_sep, s_zh_in_sum)
 
 
 
349
 
350
  yield (
351
- gr.update(interactive=True, value="🚀 开始分析"), gr.update(visible=True, value="✅ 分析完成!"),
352
- general_html, char_html, rating_html,
353
- gr.update(value=summary_text), res, current_translations_dict, tag_categories_original_order
 
 
 
 
 
 
354
  )
355
 
356
  except Exception as e:
@@ -358,13 +436,14 @@ with gr.Blocks(theme=gr.themes.Soft(), title="AI 图像标签分析器", css=cus
358
  tb_str = traceback.format_exc()
359
  print(f"处理时发生错误: {e}\n{tb_str}")
360
  yield (
361
- gr.update(visible=True, value=f"❌ 处理失败: {str(e)}"),
362
  gr.update(interactive=True, value="🚀 开始分析"),
 
363
  "<p>处理出错</p>", "<p>处理出错</p>", "<p>处理出错</p>",
364
  gr.update(value=f"错误: {str(e)}", placeholder="分析失败..."),
365
  {}, {}, {}
366
  )
367
 
 
368
  def update_summary_display(
369
  s_gen, s_char, s_rat, s_sep, s_zh_in_sum,
370
  current_res_from_state, current_translations_from_state
@@ -378,16 +457,20 @@ with gr.Blocks(theme=gr.themes.Soft(), title="AI 图像标签分析器", css=cus
378
  )
379
  return gr.update(value=new_summary_text)
380
 
381
-
382
- translation_inputs = [enable_translation_cb, system_key_input, tencent_id_input, tencent_key_input, baidu_json_input]
383
-
384
  btn.click(
385
  process_image_and_generate_outputs,
386
- inputs=[img_in, gen_slider, char_slider, show_tag_scores,
387
- sum_general, sum_char, sum_rating, sum_sep, sum_show_zh] + translation_inputs,
388
- outputs=[btn, processing_info,
389
- out_general, out_char, out_rating, out_summary,
390
- state_res, state_translations_dict, state_tag_categories_for_translation]
 
 
 
 
 
 
391
  )
392
 
393
  summary_controls = [sum_general, sum_char, sum_rating, sum_sep, sum_show_zh]
@@ -395,9 +478,13 @@ with gr.Blocks(theme=gr.themes.Soft(), title="AI 图像标签分析器", css=cus
395
  ctrl.change(
396
  fn=update_summary_display,
397
  inputs=summary_controls + [state_res, state_translations_dict],
398
- outputs=[out_summary]
 
399
  )
400
-
 
 
 
401
  if __name__ == "__main__":
402
  if tagger_instance is None:
403
  print("CRITICAL: Tagger 未能初始化,应用功能将受限。请检查之前的错误信息。")
 
1
  import os
2
+ import json
3
  import gradio as gr
4
  import huggingface_hub
5
  import numpy as np
 
8
  from PIL import Image
9
  from huggingface_hub import login
10
 
 
11
  from translator import translate_texts
12
 
13
  # ------------------------------------------------------------------
 
19
 
20
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
21
  if HF_TOKEN:
22
+ login(token=HF_TOKEN)
 
 
 
23
  else:
24
  print("⚠️ 未检测到 HF_TOKEN,私有模型可能下载失败")
25
 
 
122
  # Gradio UI
123
  # ------------------------------------------------------------------
124
  custom_css = """
125
+ .label-container {
126
+ max-height: 300px;
127
+ overflow-y: auto;
128
+ border: 1px solid #ddd;
129
+ padding: 10px;
130
+ border-radius: 5px;
131
+ background-color: #f9f9f9;
132
+ }
133
+ .tag-item {
134
+ display: flex;
135
+ justify-content: space-between;
136
+ align-items: center;
137
+ margin: 2px 0;
138
+ padding: 2px 5px;
139
+ border-radius: 3px;
140
+ background-color: #fff;
141
+ transition: background-color 0.2s;
142
+ }
143
+ .tag-item:hover {
144
+ background-color: #f0f0f0;
145
+ }
146
+ .tag-en {
147
+ font-weight: bold;
148
+ color: #333;
149
+ cursor: pointer; /* Indicates clickable */
150
+ }
151
+ .tag-zh {
152
+ color: #666;
153
+ margin-left: 10px;
154
+ }
155
+ .tag-score {
156
+ color: #999;
157
+ font-size: 0.9em;
158
+ }
159
+ .btn-analyze-container { /* Custom class for analyze button container */
160
+ margin-top: 15px;
161
+ margin-bottom: 15px;
162
+ }
163
  """
164
 
165
  _js_functions = """
166
  function copyToClipboard(text) {
167
+ // --- 调试信息 ---
168
+ console.log('copyToClipboard function was called.');
169
+ console.log('Received text:', text);
170
+ // console.trace(); // 如果需要更详细的调用栈信息,可以取消这行注释
171
+
172
+ // --- 保护性检查 ---
173
+ // 如果 text 未定义或为 null,则不执行后续操作,并打印警告
174
  if (typeof text === 'undefined' || text === null) {
175
+ console.warn('copyToClipboard was called with undefined or null text. Aborting this specific copy operation.');
176
+ // 在这种情况下,我们不应该尝试复制,也不应该显示“已复制”的提示
177
  return;
178
  }
179
+
180
  navigator.clipboard.writeText(text).then(() => {
181
+ // console.log('Tag copied to clipboard: ' + text); // 成功复制的日志(可选)
182
  const feedback = document.createElement('div');
183
+
184
+ // 确保 text 是字符串类型,再进行 substring 操作
185
+ let displayText = String(text); // 将 text 转换为字符串以防万一
186
  displayText = displayText.substring(0, 30) + (displayText.length > 30 ? '...' : '');
187
+
188
  feedback.textContent = '已复制: ' + displayText;
189
+ feedback.style.position = 'fixed';
190
+ feedback.style.bottom = '20px';
191
+ feedback.style.left = '50%';
192
+ feedback.style.transform = 'translateX(-50%)';
193
+ feedback.style.backgroundColor = '#4CAF50';
194
+ feedback.style.color = 'white';
195
+ feedback.style.padding = '10px 20px';
196
+ feedback.style.borderRadius = '5px';
197
+ feedback.style.zIndex = '10000';
198
+ feedback.style.transition = 'opacity 0.5s ease-out';
199
  document.body.appendChild(feedback);
200
  setTimeout(() => {
201
  feedback.style.opacity = '0';
202
+ setTimeout(() => {
203
+ if (document.body.contains(feedback)) { // 确保元素还在DOM中
204
+ document.body.removeChild(feedback);
205
+ }
206
+ }, 500);
207
  }, 1500);
208
  }).catch(err => {
209
+ console.error('Failed to copy tag. Error:', err, 'Attempted to copy text:', text);
210
+ // 可以考虑也给用户一��错误提示,但原版 alert 可能体验不佳
211
+ // alert('复制失败: ' + err);
212
+ const errorFeedback = document.createElement('div');
213
+ errorFeedback.textContent = '复制操作失败!'; // 更友好的错误提示
214
+ errorFeedback.style.position = 'fixed';
215
+ errorFeedback.style.bottom = '20px';
216
+ errorFeedback.style.left = '50%';
217
+ errorFeedback.style.transform = 'translateX(-50%)';
218
+ errorFeedback.style.backgroundColor = '#D32F2F'; // 红色背景表示错误
219
+ errorFeedback.style.color = 'white';
220
+ errorFeedback.style.padding = '10px 20px';
221
+ errorFeedback.style.borderRadius = '5px';
222
+ errorFeedback.style.zIndex = '10000';
223
+ errorFeedback.style.transition = 'opacity 0.5s ease-out';
224
+ document.body.appendChild(errorFeedback);
225
+ setTimeout(() => {
226
+ errorFeedback.style.opacity = '0';
227
+ setTimeout(() => {
228
+ if (document.body.contains(errorFeedback)) {
229
+ document.body.removeChild(errorFeedback);
230
+ }
231
+ }, 500);
232
+ }, 2500);
233
  });
234
  }
235
  """
 
253
  gen_slider = gr.Slider(0, 1, value=0.35, step=0.01, label="通用标签阈值", info="越高 → 标签更少更准")
254
  char_slider = gr.Slider(0, 1, value=0.85, step=0.01, label="角色标签阈值", info="推荐保持较高阈值")
255
  show_tag_scores = gr.Checkbox(True, label="在列表中显示标签置信度")
256
+
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  with gr.Accordion("📊 标签汇总设置", open=True):
258
  gr.Markdown("选择要包含在下方汇总文本框中的标签类别:")
259
  with gr.Row():
 
283
  show_copy_button=True
284
  )
285
 
286
+ # ----------------- 辅助函数 -----------------
287
+ def format_tags_html(tags_dict, translations_list, category_name, show_scores=True, show_translation_in_list=True):
288
  if not tags_dict:
289
  return "<p>暂无标签</p>"
290
 
291
  html = '<div class="label-container">'
292
+
293
+ if not isinstance(translations_list, list):
294
+ translations_list = []
295
+
296
  tag_keys = list(tags_dict.keys())
297
 
298
  for i, tag in enumerate(tag_keys):
299
  score = tags_dict[tag]
300
+ escaped_tag = tag.replace("'", "\\'") # Escape for JS
301
 
302
  html += '<div class="tag-item">'
303
  tag_display_html = f'<span class="tag-en" onclick="copyToClipboard(\'{escaped_tag}\')">{tag}</span>'
304
 
305
+ if show_translation_in_list and i < len(translations_list) and translations_list[i]:
306
+ tag_display_html += f'<span class="tag-zh">({translations_list[i]})</span>'
 
 
307
 
308
  html += f'<div>{tag_display_html}</div>'
309
  if show_scores:
 
316
  current_res, current_translations_dict,
317
  s_gen, s_char, s_rat, s_sep_type, s_show_zh
318
  ):
319
+ if not current_res:
320
+ return "请先分析图像或选择要汇总的标签类别。"
321
 
322
  summary_parts = []
323
+ separators = {"逗号": ", ", "换行": "\n", "空格": " "}
324
+ separator = separators.get(s_sep_type, ", ")
325
 
326
  categories_to_summarize = []
327
  if s_gen: categories_to_summarize.append("general")
328
  if s_char: categories_to_summarize.append("characters")
329
  if s_rat: categories_to_summarize.append("ratings")
330
 
331
+ if not categories_to_summarize:
332
+ return "请至少选择一个标签类别进行汇总。"
333
 
334
  for cat_key in categories_to_summarize:
335
  if current_res.get(cat_key):
 
338
  cat_translations = current_translations_dict.get(cat_key, [])
339
 
340
  for i, en_tag in enumerate(cat_tags_en):
341
+ if s_show_zh and i < len(cat_translations) and cat_translations[i]:
342
+ tags_to_join.append(f"{en_tag}({cat_translations[i]})")
 
 
343
  else:
344
  tags_to_join.append(en_tag)
345
+ if tags_to_join: # only add if there are tags for this category
346
  summary_parts.append(separator.join(tags_to_join))
347
 
348
+ # Join parts with double newline for readability if multiple categories present and separator is not newline
349
+ joiner = "\n\n" if separator != "\n" and len(summary_parts) > 1 else separator if separator == "\n" else " "
350
+
351
  final_summary = joiner.join(summary_parts)
352
  return final_summary if final_summary else "选定的类别中没有找到标签。"
353
 
354
+
355
+ # ----------------- 主要处理回调 -----------------
356
  def process_image_and_generate_outputs(
357
  img, g_th, c_th, s_scores, # Main inputs
358
+ s_gen, s_char, s_rat, s_sep, s_zh_in_sum
 
 
359
  ):
 
 
 
 
 
 
360
  if img is None:
361
+ yield (
362
+ gr.update(interactive=True, value="🚀 开始分析"),
363
+ gr.update(visible=True, value="❌ 请先上传图片。"),
364
+ "", "", "", "",
365
+ gr.update(placeholder="请先上传图片并开始分析..."),
366
+ {}, {}, {}
367
+ )
368
  return
369
 
370
  if tagger_instance is None:
371
+ yield (
372
+ gr.update(interactive=True, value="🚀 开始分析"),
373
+ gr.update(visible=True, value="❌ 分析器未成功初始化,请检查控制台错误。"),
374
+ "", "", "", "",
375
+ gr.update(placeholder="分析器初始化失败..."),
376
+ {}, {}, {}
377
+ )
378
  return
379
 
380
  yield (
381
  gr.update(interactive=False, value="🔄 处理中..."),
382
  gr.update(visible=True, value="🔄 正在分析图像,请稍候..."),
383
+ gr.HTML(value="<p>分析中...</p>"), # General
384
+ gr.HTML(value="<p>分析中...</p>"), # Character
385
+ gr.HTML(value="<p>分析中...</p>"), # Rating
386
+ gr.update(value="分析中,请稍候..."), # Summary
387
+ {}, {}, {} # Clear states initially
388
  )
389
 
390
  try:
391
+ # 1. Predict tags
392
  res, tag_categories_original_order = tagger_instance.predict(img, g_th, c_th)
393
+
394
+ all_tags_to_translate = []
395
+ for cat_key in ["general", "characters", "ratings"]:
396
+ all_tags_to_translate.extend(tag_categories_original_order.get(cat_key, []))
397
+
398
+ all_translations_flat = []
399
+ if all_tags_to_translate:
400
+ all_translations_flat = translate_texts(all_tags_to_translate, src_lang="auto", tgt_lang="zh")
401
 
402
  current_translations_dict = {}
403
+ offset = 0
404
+ for cat_key in ["general", "characters", "ratings"]:
405
+ cat_original_tags = tag_categories_original_order.get(cat_key, [])
406
+ num_tags_in_cat = len(cat_original_tags)
407
+ if num_tags_in_cat > 0:
408
+ current_translations_dict[cat_key] = all_translations_flat[offset : offset + num_tags_in_cat]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  offset += num_tags_in_cat
410
+ else:
411
+ current_translations_dict[cat_key] = []
 
412
 
413
+ general_html = format_tags_html(res.get("general", {}), current_translations_dict.get("general", []), "general", s_scores, True)
414
+ char_html = format_tags_html(res.get("characters", {}), current_translations_dict.get("characters", []), "characters", s_scores, True)
415
+ rating_html = format_tags_html(res.get("ratings", {}), current_translations_dict.get("ratings", []), "ratings", s_scores, True)
416
 
417
+ summary_text = generate_summary_text_content(
418
+ res, current_translations_dict,
419
+ s_gen, s_char, s_rat, s_sep, s_zh_in_sum
420
+ )
421
 
422
  yield (
423
+ gr.update(interactive=True, value="🚀 开始分析"),
424
+ gr.update(visible=True, value="✅ 分析完成!"),
425
+ general_html,
426
+ char_html,
427
+ rating_html,
428
+ gr.update(value=summary_text),
429
+ res,
430
+ current_translations_dict,
431
+ tag_categories_original_order
432
  )
433
 
434
  except Exception as e:
 
436
  tb_str = traceback.format_exc()
437
  print(f"处理时发生错误: {e}\n{tb_str}")
438
  yield (
 
439
  gr.update(interactive=True, value="🚀 开始分析"),
440
+ gr.update(visible=True, value=f"❌ 处理失败: {str(e)}"),
441
  "<p>处理出错</p>", "<p>处理出错</p>", "<p>处理出错</p>",
442
  gr.update(value=f"错误: {str(e)}", placeholder="分析失败..."),
443
  {}, {}, {}
444
  )
445
 
446
+ # ----------------- 更新汇总文本的回调 -----------------
447
  def update_summary_display(
448
  s_gen, s_char, s_rat, s_sep, s_zh_in_sum,
449
  current_res_from_state, current_translations_from_state
 
457
  )
458
  return gr.update(value=new_summary_text)
459
 
460
+ # ----------------- 绑定事件 -----------------
 
 
461
  btn.click(
462
  process_image_and_generate_outputs,
463
+ inputs=[
464
+ img_in, gen_slider, char_slider, show_tag_scores,
465
+ sum_general, sum_char, sum_rating, sum_sep, sum_show_zh
466
+ ],
467
+ outputs=[
468
+ btn, processing_info,
469
+ out_general, out_char, out_rating,
470
+ out_summary,
471
+ state_res, state_translations_dict, state_tag_categories_for_translation
472
+ ],
473
+ # show_progress="full" # Gradio's built-in progress
474
  )
475
 
476
  summary_controls = [sum_general, sum_char, sum_rating, sum_sep, sum_show_zh]
 
478
  ctrl.change(
479
  fn=update_summary_display,
480
  inputs=summary_controls + [state_res, state_translations_dict],
481
+ outputs=[out_summary],
482
+ # show_progress=False # Typically fast, no need for progress indicator
483
  )
484
+
485
+ # ------------------------------------------------------------------
486
+ # 启动
487
+ # ------------------------------------------------------------------
488
  if __name__ == "__main__":
489
  if tagger_instance is None:
490
  print("CRITICAL: Tagger 未能初始化,应用功能将受限。请检查之前的错误信息。")
translator.py CHANGED
@@ -1,141 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import hashlib, hmac, json, os, random, time
2
  from datetime import datetime
3
- from typing import List, Sequence, Optional, Dict, Any
4
 
5
  import requests
6
 
7
- TRANSLATOR_ACCESS_KEY = os.environ.get("TRANSLATOR_ACCESS_KEY")
8
-
9
- TENCENT_SECRET_ID = os.environ.get("TENCENT_SECRET_ID")
 
10
  TENCENT_SECRET_KEY = os.environ.get("TENCENT_SECRET_KEY")
11
  TENCENT_TRANSLATE_URL = os.environ.get("TENCENT_TRANSLATE_URL", "https://tmt.tencentcloudapi.com")
12
 
13
  BAIDU_TRANSLATE_URL = os.environ.get("BAIDU_TRANSLATE_URL", "https://fanyi-api.baidu.com/api/trans/vip/translate")
14
- try:
15
- BAIDU_CREDENTIALS_DEFAULT = json.loads(os.environ.get("BAIDU_CREDENTIALS_JSON", "[]"))
16
- except json.JSONDecodeError:
17
- BAIDU_CREDENTIALS_DEFAULT = []
 
 
 
 
 
 
 
18
 
 
 
 
19
  def _sign(key: bytes, msg: str) -> bytes:
20
  return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
21
 
22
  def _tc3_signature(secret_key: str, date: str, service: str, string_to_sign: str) -> str:
23
- secret_date = _sign(("TC3" + secret_key).encode(), date)
24
- secret_service = _sign(secret_date, service)
25
- secret_signing = _sign(secret_service, "tc3_request")
26
  return hmac.new(secret_signing, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
27
 
28
- def _translate_with_tencent(texts: Sequence[str], src: str, tgt: str, secret_id: str, secret_key: str) -> Optional[List[str]]:
29
- if not (secret_id and secret_key): return None
30
-
31
- service, host, action, version, region = "tmt", "tmt.tencentcloudapi.com", "TextTranslate", "2018-03-21", "ap-beijing"
32
- ts = int(time.time())
33
- date = datetime.utcfromtimestamp(ts).strftime("%Y-%m-%d")
34
-
35
- payload = {"SourceText": "\n".join(texts), "Source": src, "Target": tgt, "ProjectId": 0}
 
 
 
 
 
 
 
 
 
 
 
36
  payload_str = json.dumps(payload, ensure_ascii=False)
37
-
38
- canonical_request = "POST\n/\n\n" + \
39
- f"content-type:application/json; charset=utf-8\nhost:{host}\nx-tc-action:{action.lower()}\n\n" + \
40
- "content-type;host;x-tc-action\n" + \
41
- hashlib.sha256(payload_str.encode()).hexdigest()
42
-
43
- credential_scope = f"{date}/{service}/tc3_request"
44
- string_to_sign = "TC3-HMAC-SHA256\n" + str(ts) + "\n" + credential_scope + "\n" + \
45
- hashlib.sha256(canonical_request.encode()).hexdigest()
46
-
47
- signature = _tc3_signature(secret_key, date, service, string_to_sign)
48
-
49
- authorization = f"TC3-HMAC-SHA256 Credential={secret_id}/{credential_scope}, " + \
50
- f"SignedHeaders=content-type;host;x-tc-action, Signature={signature}"
51
-
 
 
 
 
 
 
 
 
 
 
 
52
  headers = {
53
- "Authorization": authorization, "Content-Type": "application/json; charset=utf-8",
54
- "Host": host, "X-TC-Action": action, "X-TC-Timestamp": str(ts),
55
- "X-TC-Version": version, "X-TC-Region": region,
 
 
 
 
56
  }
57
 
 
58
  try:
59
  resp = requests.post(TENCENT_TRANSLATE_URL, headers=headers, data=payload_str, timeout=8)
60
  resp.raise_for_status()
61
  data = resp.json()
62
- if "Response" in data and "TargetText" in data["Response"]:
63
- return data["Response"]["TargetText"].split("\n")
64
- else:
65
- print(f"[translator] Tencent API abnormal response: {data}")
66
- return None
67
  except Exception as e:
68
  print(f"[translator] Tencent API error → {e}")
69
  return None
70
 
71
- def _translate_with_baidu(texts: Sequence[str], src: str, tgt: str, baidu_credentials: List[Dict[str, str]]) -> Optional[List[str]]:
72
- if not baidu_credentials: return None
73
-
74
- cred = random.choice(baidu_credentials)
75
- app_id, secret_key = cred.get("app_id"), cred.get("secret_key")
76
-
77
- if not (app_id and secret_key): return None
78
-
79
- salt = random.randint(32768, 65536)
80
  query = "\n".join(texts)
81
- sign = hashlib.md5((app_id + query + str(salt) + secret_key).encode()).hexdigest()
82
- params = {"q": query, "from": src, "to": tgt, "appid": app_id, "salt": salt, "sign": sign}
83
-
 
 
84
  try:
85
  resp = requests.get(BAIDU_TRANSLATE_URL, params=params, timeout=8)
86
  resp.raise_for_status()
87
  data = resp.json()
88
- if "trans_result" in data:
89
- return [item["dst"] for item in data["trans_result"]]
90
- else:
91
- print(f"[translator] Baidu API abnormal response: {data}")
92
- return None
93
  except Exception as e:
94
  print(f"[translator] Baidu API error → {e}")
95
  return None
96
 
97
-
 
 
98
  def translate_texts(texts: Sequence[str],
99
  src_lang: str = "auto",
100
- tgt_lang: str = "zh",
101
- system_key_input: Optional[str] = None,
102
- tencent_id: Optional[str] = None,
103
- tencent_key: Optional[str] = None,
104
- baidu_creds_json_str: Optional[str] = None) -> List[str]:
105
  if not texts:
106
  return []
107
 
108
- use_tencent_id, use_tencent_key = None, None
109
- use_baidu_creds = []
110
-
111
- if tencent_id and tencent_key:
112
- use_tencent_id, use_tencent_key = tencent_id, tencent_key
113
- print("[translator] Using custom Tencent API key.")
114
- if baidu_creds_json_str:
115
- try:
116
- creds = json.loads(baidu_creds_json_str)
117
- if isinstance(creds, list) and all(isinstance(d, dict) for d in creds):
118
- use_baidu_creds = creds
119
- print("[translator] Using custom Baidu API key(s).")
120
- else:
121
- print("[translator] Warning: Custom Baidu credentials format is incorrect.")
122
- except json.JSONDecodeError:
123
- print("[translator] Warning: Failed to parse custom Baidu credentials JSON.")
124
-
125
- elif TRANSLATOR_ACCESS_KEY and system_key_input == TRANSLATOR_ACCESS_KEY:
126
- print("[translator] System access key validated. Using system-configured API keys.")
127
- use_tencent_id, use_tencent_key = TENCENT_SECRET_ID, TENCENT_SECRET_KEY
128
- use_baidu_creds = BAIDU_CREDENTIALS_DEFAULT
129
-
130
- else:
131
- print("[translator] Translation disabled: No valid API keys or system key provided.")
132
- return list(texts)
133
-
134
- translated_texts = None
135
- if use_tencent_id and use_tencent_key:
136
- translated_texts = _translate_with_tencent(texts, src_lang, tgt_lang, use_tencent_id, use_tencent_key)
137
-
138
- if translated_texts is None and use_baidu_creds:
139
- translated_texts = _translate_with_baidu(texts, src_lang, tgt_lang, use_baidu_creds)
140
-
141
- return translated_texts or list(texts)
 
1
+ """
2
+ translator.py
3
+ 腾讯云 + 百度翻译 API 轮询封装
4
+ ⚠️ 需在 HF 空间的 “Variables” 页设置以下环境变量
5
+ ------------------------------------------------------------------
6
+ TENCENT_SECRET_ID 腾讯云 SecretId
7
+ TENCENT_SECRET_KEY 腾讯云 SecretKey
8
+ TENCENT_TRANSLATE_URL (可选) 默认 https://tmt.tencentcloudapi.com
9
+ BAIDU_TRANSLATE_URL (可选) 默认 https://fanyi-api.baidu.com/api/trans/vip/translate
10
+ BAIDU_CREDENTIALS_JSON 形如:
11
+ [
12
+ {"app_id": "xxxx", "secret_key": "yyyy"},
13
+ {"app_id": "aaaa", "secret_key": "bbbb"}
14
+ ]
15
+ ------------------------------------------------------------------
16
+ """
17
  import hashlib, hmac, json, os, random, time
18
  from datetime import datetime
19
+ from typing import List, Sequence, Optional
20
 
21
  import requests
22
 
23
+ # ------------------------------------------------------------------
24
+ # 读取环境变量
25
+ # ------------------------------------------------------------------
26
+ TENCENT_SECRET_ID = os.environ.get("TENCENT_SECRET_ID")
27
  TENCENT_SECRET_KEY = os.environ.get("TENCENT_SECRET_KEY")
28
  TENCENT_TRANSLATE_URL = os.environ.get("TENCENT_TRANSLATE_URL", "https://tmt.tencentcloudapi.com")
29
 
30
  BAIDU_TRANSLATE_URL = os.environ.get("BAIDU_TRANSLATE_URL", "https://fanyi-api.baidu.com/api/trans/vip/translate")
31
+ BAIDU_CREDENTIALS = json.loads(os.environ.get("BAIDU_CREDENTIALS_JSON", "[]"))
32
+
33
+ # 内部轮询索引
34
+ _baidu_idx: int = 0
35
+ def _next_baidu_cred():
36
+ global _baidu_idx
37
+ if not BAIDU_CREDENTIALS:
38
+ return None
39
+ cred = BAIDU_CREDENTIALS[_baidu_idx]
40
+ _baidu_idx = (_baidu_idx + 1) % len(BAIDU_CREDENTIALS)
41
+ return cred
42
 
43
+ # ------------------------------------------------------------------
44
+ # 腾讯翻译
45
+ # ------------------------------------------------------------------
46
  def _sign(key: bytes, msg: str) -> bytes:
47
  return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
48
 
49
  def _tc3_signature(secret_key: str, date: str, service: str, string_to_sign: str) -> str:
50
+ secret_date = _sign(("TC3" + secret_key).encode(), date)
51
+ secret_service = _sign(secret_date, service)
52
+ secret_signing = _sign(secret_service, "tc3_request")
53
  return hmac.new(secret_signing, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
54
 
55
+ def _translate_with_tencent(texts: Sequence[str], src="auto", tgt="zh") -> Optional[List[str]]:
56
+ """优先使用腾讯云翻译。失败返回 None"""
57
+ if not (TENCENT_SECRET_ID and TENCENT_SECRET_KEY):
58
+ return None # 未配置凭证
59
+ service = "tmt"
60
+ host = "tmt.tencentcloudapi.com"
61
+ action = "TextTranslate"
62
+ version = "2018-03-21"
63
+ region = "ap-beijing"
64
+ ts = int(time.time())
65
+ date = datetime.utcfromtimestamp(ts).strftime("%Y-%m-%d")
66
+ algorithm = "TC3-HMAC-SHA256"
67
+
68
+ payload = {
69
+ "SourceText": "\n".join(texts),
70
+ "Source": src,
71
+ "Target": tgt,
72
+ "ProjectId": 0,
73
+ }
74
  payload_str = json.dumps(payload, ensure_ascii=False)
75
+
76
+ # ---------- step‑1 canonical request ----------
77
+ canonical_request = "\n".join([
78
+ "POST",
79
+ "/",
80
+ "",
81
+ f"content-type:application/json; charset=utf-8\nhost:{host}\nx-tc-action:{action.lower()}\n",
82
+ "content-type;host;x-tc-action",
83
+ hashlib.sha256(payload_str.encode()).hexdigest(),
84
+ ])
85
+
86
+ # ---------- step‑2 string to sign ----------
87
+ credential_scope = f"{date}/{service}/tc3_request"
88
+ string_to_sign = "\n".join([
89
+ algorithm, str(ts), credential_scope,
90
+ hashlib.sha256(canonical_request.encode()).hexdigest(),
91
+ ])
92
+
93
+ # ---------- step‑3 signature ----------
94
+ signature = _tc3_signature(TENCENT_SECRET_KEY, date, service, string_to_sign)
95
+
96
+ # ---------- step‑4 headers ----------
97
+ authorization = (
98
+ f"{algorithm} Credential={TENCENT_SECRET_ID}/{credential_scope}, "
99
+ f"SignedHeaders=content-type;host;x-tc-action, Signature={signature}"
100
+ )
101
  headers = {
102
+ "Authorization": authorization,
103
+ "Content-Type": "application/json; charset=utf-8",
104
+ "Host": host,
105
+ "X-TC-Action": action,
106
+ "X-TC-Timestamp": str(ts),
107
+ "X-TC-Version": version,
108
+ "X-TC-Region": region,
109
  }
110
 
111
+ # ---------- request ----------
112
  try:
113
  resp = requests.post(TENCENT_TRANSLATE_URL, headers=headers, data=payload_str, timeout=8)
114
  resp.raise_for_status()
115
  data = resp.json()
116
+ return data["Response"]["TargetText"].split("\n")
 
 
 
 
117
  except Exception as e:
118
  print(f"[translator] Tencent API error → {e}")
119
  return None
120
 
121
+ # ------------------------------------------------------------------
122
+ # 百度翻译
123
+ # ------------------------------------------------------------------
124
+ def _translate_with_baidu(texts: Sequence[str], src="auto", tgt="zh") -> Optional[List[str]]:
125
+ creds = _next_baidu_cred()
126
+ if creds is None:
127
+ return None # 未配置凭证
128
+ app_id, secret_key = creds["app_id"], creds["secret_key"]
129
+ salt = random.randint(32768, 65536)
130
  query = "\n".join(texts)
131
+ sign = hashlib.md5((app_id + query + str(salt) + secret_key).encode()).hexdigest()
132
+ params = {
133
+ "q": query, "from": src, "to": tgt,
134
+ "appid": app_id, "salt": salt, "sign": sign,
135
+ }
136
  try:
137
  resp = requests.get(BAIDU_TRANSLATE_URL, params=params, timeout=8)
138
  resp.raise_for_status()
139
  data = resp.json()
140
+ return [item["dst"] for item in data["trans_result"]]
 
 
 
 
141
  except Exception as e:
142
  print(f"[translator] Baidu API error → {e}")
143
  return None
144
 
145
+ # ------------------------------------------------------------------
146
+ # 对外统一函数
147
+ # ------------------------------------------------------------------
148
  def translate_texts(texts: Sequence[str],
149
  src_lang: str = "auto",
150
+ tgt_lang: str = "zh") -> List[str]:
151
+ """
152
+ 优先 Tencent 失败再 Baidu → 如果都失败,返回原文。
153
+ """
 
154
  if not texts:
155
  return []
156
 
157
+ out = _translate_with_tencent(texts, src_lang, tgt_lang)
158
+ if out is None:
159
+ out = _translate_with_baidu(texts, src_lang, tgt_lang)
160
+ return out or list(texts)