anytrans_client.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  1. """
  2. 通义多模态翻译客户端
  3. 基于阿里云 AnyTrans API (2025-07-07版本)
  4. 文档:https://help.aliyun.com/zh/model-studio/api-anytrans-2025-07-07
  5. """
  6. import logging
  7. from typing import Optional, List, Dict, Any
  8. from alibabacloud_anytrans20250707.client import Client as AnyTransClient
  9. from alibabacloud_tea_openapi import models as open_api_models
  10. from alibabacloud_anytrans20250707 import models as anytrans_models
  11. from alibabacloud_tea_util import models as util_models
  12. logger = logging.getLogger(__name__)
  13. class TongyiAnyTransClient:
  14. """通义多模态翻译客户端"""
  15. def __init__(self, access_key_id: str, access_key_secret: str, workspace_id: str):
  16. """
  17. 初始化客户端
  18. Args:
  19. access_key_id: 阿里云 AccessKey ID
  20. access_key_secret: 阿里云 AccessKey Secret
  21. workspace_id: 百炼工作空间 ID
  22. """
  23. if not all([access_key_id, access_key_secret, workspace_id]):
  24. raise ValueError("access_key_id, access_key_secret and workspace_id are required")
  25. self.workspace_id = workspace_id
  26. # 创建配置
  27. config = open_api_models.Config(
  28. access_key_id=access_key_id,
  29. access_key_secret=access_key_secret,
  30. endpoint='anytrans.cn-beijing.aliyuncs.com'
  31. )
  32. # 创建客户端
  33. self.client = AnyTransClient(config)
  34. logger.info("TongyiAnyTransClient initialized successfully")
  35. def text_translate(
  36. self,
  37. source_text: str,
  38. target_language: str,
  39. source_language: str = "auto",
  40. scene: str = "mt-turbo",
  41. terminologies: Optional[List[Dict[str, str]]] = None,
  42. domain_hint: Optional[str] = None
  43. ) -> Dict[str, Any]:
  44. """
  45. 文本翻译
  46. Args:
  47. source_text: 源文本
  48. target_language: 目标语言(如:en, zh, ja)
  49. source_language: 源语言(默认auto自动检测)
  50. scene: 模型类型(mt-plus专业版/mt-turbo轻量版)
  51. terminologies: 术语干预列表
  52. domain_hint: 领域提示
  53. Returns:
  54. 翻译结果字典
  55. """
  56. try:
  57. # 构建基础请求
  58. request = anytrans_models.TextTranslateRequest(
  59. workspace_id=self.workspace_id,
  60. source_language=source_language,
  61. target_language=target_language,
  62. text=source_text,
  63. scene=scene
  64. )
  65. # 添加扩展参数
  66. if terminologies or domain_hint:
  67. ext = anytrans_models.TextTranslateRequestExt()
  68. if terminologies:
  69. # 转换术语格式
  70. term_objects = []
  71. for term in terminologies:
  72. term_obj = anytrans_models.TextTranslateRequestExtTerminologies()
  73. term_obj.src = term["src"]
  74. term_obj.tgt = term["tgt"]
  75. term_objects.append(term_obj)
  76. ext.terminologies = term_objects
  77. if domain_hint:
  78. ext.domain_hint = domain_hint
  79. request.ext = ext
  80. # 直接调用
  81. response = self.client.text_translate(request)
  82. if response.status_code != 200:
  83. raise Exception(f"API调用失败: {response.status_code}")
  84. body = response.body
  85. return {
  86. "translated_text": body.data.translation,
  87. "detected_language": getattr(body.data, 'detected_language', None),
  88. "usage": {
  89. "input_tokens": body.data.usage.input_tokens if hasattr(body.data, 'usage') else 0,
  90. "output_tokens": body.data.usage.output_tokens if hasattr(body.data, 'usage') else 0
  91. }
  92. }
  93. except Exception as e:
  94. logger.error(f"文本翻译失败: {str(e)}")
  95. raise Exception(f"文本翻译失败: {str(e)}")
  96. def batch_translate(
  97. self,
  98. texts: List[str],
  99. target_language: str,
  100. source_language: str = "auto",
  101. scene: str = "mt-turbo"
  102. ) -> List[Dict[str, Any]]:
  103. """
  104. 批量文本翻译
  105. Args:
  106. texts: 文本列表(最多100条)
  107. target_language: 目标语言
  108. source_language: 源语言
  109. scene: 模型类型
  110. Returns:
  111. 翻译结果列表
  112. """
  113. try:
  114. request = anytrans_models.BatchTranslateRequest(
  115. workspace_id=self.workspace_id,
  116. source_language=source_language,
  117. target_language=target_language,
  118. source_texts=texts,
  119. scene=scene
  120. )
  121. runtime = util_models.RuntimeOptions()
  122. response = self.client.batch_translate_with_options(request, runtime)
  123. if response.status_code != 200:
  124. raise Exception(f"API调用失败: {response.status_code}")
  125. body = response.body
  126. results = []
  127. for idx, item in enumerate(body.data.translations):
  128. results.append({
  129. "index": idx,
  130. "source_text": texts[idx],
  131. "translated_text": item.translated_text,
  132. "status": "completed",
  133. "usage": {
  134. "input_tokens": item.usage.input_tokens if hasattr(item, 'usage') else 0,
  135. "output_tokens": item.usage.output_tokens if hasattr(item, 'usage') else 0
  136. }
  137. })
  138. return results
  139. except Exception as e:
  140. logger.error(f"批量翻译失败: {str(e)}")
  141. raise Exception(f"批量翻译失败: {str(e)}")
  142. def submit_long_text_task(
  143. self,
  144. source_text: str,
  145. target_language: str,
  146. source_language: str = "auto",
  147. scene: str = "mt-turbo"
  148. ) -> str:
  149. """
  150. 提交长文本翻译任务
  151. Args:
  152. source_text: 源文本(最大100万字符)
  153. target_language: 目标语言
  154. source_language: 源语言
  155. scene: 模型类型
  156. Returns:
  157. 任务ID
  158. """
  159. try:
  160. request = anytrans_models.SubmitLongTextTranslateTaskRequest(
  161. workspace_id=self.workspace_id,
  162. source_language=source_language,
  163. target_language=target_language,
  164. source_text=source_text,
  165. scene=scene
  166. )
  167. runtime = util_models.RuntimeOptions()
  168. response = self.client.submit_long_text_translate_task_with_options(request, runtime)
  169. if response.status_code != 200:
  170. raise Exception(f"API调用失败: {response.status_code}")
  171. return response.body.data.task_id
  172. except Exception as e:
  173. logger.error(f"提交长文本任务失败: {str(e)}")
  174. raise Exception(f"提交长文本任务失败: {str(e)}")
  175. def get_long_text_result(self, task_id: str) -> Dict[str, Any]:
  176. """
  177. 获取长文本翻译结果
  178. Args:
  179. task_id: 任务ID
  180. Returns:
  181. 任务结果
  182. """
  183. try:
  184. request = anytrans_models.GetLongTextTranslateTaskRequest(
  185. workspace_id=self.workspace_id,
  186. task_id=task_id
  187. )
  188. runtime = util_models.RuntimeOptions()
  189. response = self.client.get_long_text_translate_task_with_options(request, runtime)
  190. if response.status_code != 200:
  191. raise Exception(f"API调用失败: {response.status_code}")
  192. body = response.body
  193. data = body.data
  194. return {
  195. "task_id": task_id,
  196. "status": data.status,
  197. "translated_text": data.translated_text if hasattr(data, 'translated_text') else None,
  198. "progress": data.progress if hasattr(data, 'progress') else 0,
  199. "error_message": data.error_message if hasattr(data, 'error_message') else None,
  200. "usage": {
  201. "input_tokens": data.usage.input_tokens if hasattr(data, 'usage') else 0,
  202. "output_tokens": data.usage.output_tokens if hasattr(data, 'usage') else 0
  203. } if hasattr(data, 'usage') else None
  204. }
  205. except Exception as e:
  206. logger.error(f"获取长文本结果失败: {str(e)}")
  207. raise Exception(f"获取长文本结果失败: {str(e)}")
  208. def submit_image_task(
  209. self,
  210. image_url: str,
  211. target_languages: List[str],
  212. source_language: str = "auto",
  213. scene: str = "flash",
  214. terminologies: Optional[List[Dict[str, str]]] = None,
  215. domain_hint: Optional[str] = None
  216. ) -> str:
  217. """
  218. 提交图片翻译任务
  219. Args:
  220. image_url: 图片URL
  221. target_languages: 目标语言列表
  222. source_language: 源语言
  223. scene: 模型类型(general专业版/flash轻量版)
  224. terminologies: 术语干预列表
  225. domain_hint: 领域提示
  226. Returns:
  227. 任务ID
  228. """
  229. try:
  230. # 构建基础请求
  231. request = anytrans_models.SubmitImageTranslateTaskRequest(
  232. workspace_id=self.workspace_id,
  233. source_language=source_language,
  234. target_language=target_languages,
  235. text=image_url,
  236. scene=scene
  237. )
  238. # 添加扩展参数
  239. if terminologies or domain_hint:
  240. ext = anytrans_models.SubmitImageTranslateTaskRequestExt()
  241. if terminologies:
  242. term_objects = []
  243. for term in terminologies:
  244. term_obj = anytrans_models.SubmitImageTranslateTaskRequestExtTerminologies()
  245. term_obj.src = term["src"]
  246. term_obj.tgt = term["tgt"]
  247. term_objects.append(term_obj)
  248. ext.terminologies = term_objects
  249. if domain_hint:
  250. ext.domain_hint = domain_hint
  251. request.ext = ext
  252. response = self.client.submit_image_translate_task(request)
  253. if response.status_code != 200:
  254. raise Exception(f"API调用失败: {response.status_code}")
  255. return response.body.data.task_id
  256. except Exception as e:
  257. logger.error(f"提交图片翻译任务失败: {str(e)}")
  258. raise Exception(f"提交图片翻译任务失败: {str(e)}")
  259. def get_image_result(self, task_id: str) -> Dict[str, Any]:
  260. """
  261. 获取图片翻译结果
  262. Args:
  263. task_id: 任务ID
  264. Returns:
  265. 任务结果
  266. """
  267. try:
  268. request = anytrans_models.GetImageTranslateTaskRequest(
  269. workspace_id=self.workspace_id,
  270. task_id=task_id
  271. )
  272. response = self.client.get_image_translate_task(request)
  273. if response.status_code != 200:
  274. raise Exception(f"API调用失败: {response.status_code}")
  275. body = response.body
  276. data = body.data
  277. # 解析翻译结果
  278. translation = data.translation if hasattr(data, 'translation') else None
  279. result = {
  280. "task_id": task_id,
  281. "status": "completed" if translation else "processing",
  282. "translation": None
  283. }
  284. if translation:
  285. result["translation"] = {
  286. "angle": translation.angle if hasattr(translation, 'angle') else 0,
  287. "width": translation.width if hasattr(translation, 'width') else 0,
  288. "height": translation.height if hasattr(translation, 'height') else 0,
  289. "boxes_count": translation.boxes_count if hasattr(translation, 'boxes_count') else 0,
  290. "bounding_boxes": []
  291. }
  292. if hasattr(translation, 'bounding_boxes'):
  293. for box in translation.bounding_boxes:
  294. box_data = {
  295. "text": box.text if hasattr(box, 'text') else "",
  296. "translation": box.translation if hasattr(box, 'translation') else {},
  297. "confidence": box.confidence if hasattr(box, 'confidence') else 0,
  298. "up_left": {"x": box.up_left.x, "y": box.up_left.y} if hasattr(box, 'up_left') else None,
  299. "up_right": {"x": box.up_right.x, "y": box.up_right.y} if hasattr(box, 'up_right') else None,
  300. "down_left": {"x": box.down_left.x, "y": box.down_left.y} if hasattr(box, 'down_left') else None,
  301. "down_right": {"x": box.down_right.x, "y": box.down_right.y} if hasattr(box, 'down_right') else None
  302. }
  303. result["translation"]["bounding_boxes"].append(box_data)
  304. return result
  305. except Exception as e:
  306. logger.error(f"获取图片翻译结果失败: {str(e)}")
  307. raise Exception(f"获取图片翻译结果失败: {str(e)}")
  308. def submit_doc_task(
  309. self,
  310. doc_url: str,
  311. target_language: str,
  312. source_language: str = "auto",
  313. scene: str = "mt-turbo",
  314. terminologies: Optional[List[Dict[str, str]]] = None,
  315. domain_hint: Optional[str] = None,
  316. skip_img_trans: bool = False
  317. ) -> str:
  318. """
  319. 提交文档翻译任务
  320. Args:
  321. doc_url: 文档URL
  322. target_language: 目标语言
  323. source_language: 源语言
  324. scene: 模型类型
  325. terminologies: 术语干预列表
  326. domain_hint: 领域提示
  327. skip_img_trans: 是否跳过PDF中的图片翻译
  328. Returns:
  329. 任务ID
  330. """
  331. try:
  332. # 构建基础请求
  333. request = anytrans_models.SubmitDocTranslateTaskRequest(
  334. workspace_id=self.workspace_id,
  335. source_language=source_language,
  336. target_language=target_language,
  337. text=doc_url,
  338. scene=scene
  339. )
  340. # 添加扩展参数
  341. if terminologies or domain_hint or skip_img_trans:
  342. ext = anytrans_models.SubmitDocTranslateTaskRequestExt()
  343. if terminologies:
  344. term_objects = []
  345. for term in terminologies:
  346. term_obj = anytrans_models.SubmitDocTranslateTaskRequestExtTerminologies()
  347. term_obj.src = term["src"]
  348. term_obj.tgt = term["tgt"]
  349. term_objects.append(term_obj)
  350. ext.terminologies = term_objects
  351. if domain_hint:
  352. ext.domain_hint = domain_hint
  353. # 配置对象
  354. config = anytrans_models.SubmitDocTranslateTaskRequestExtConfig()
  355. config.skip_img_trans = skip_img_trans
  356. ext.config = config
  357. request.ext = ext
  358. response = self.client.submit_doc_translate_task(request)
  359. if response.status_code != 200:
  360. raise Exception(f"API调用失败: {response.status_code}")
  361. return response.body.data.task_id
  362. except Exception as e:
  363. logger.error(f"提交文档翻译任务失败: {str(e)}")
  364. raise Exception(f"提交文档翻译任务失败: {str(e)}")
  365. def get_doc_result(self, task_id: str) -> Dict[str, Any]:
  366. """
  367. 获取文档翻译结果
  368. Args:
  369. task_id: 任务ID
  370. Returns:
  371. 任务结果
  372. """
  373. try:
  374. request = anytrans_models.GetDocTranslateTaskRequest(
  375. workspace_id=self.workspace_id,
  376. task_id=task_id
  377. )
  378. response = self.client.get_doc_translate_task(request)
  379. if response.status_code != 200:
  380. raise Exception(f"API调用失败: {response.status_code}")
  381. body = response.body
  382. data = body.data
  383. result = {
  384. "task_id": task_id,
  385. "status": data.status if hasattr(data, 'status') else "processing"
  386. }
  387. # 如果翻译完成,添加结果信息
  388. if hasattr(data, 'translate_file_url') and data.translate_file_url:
  389. result["translate_file_url"] = data.translate_file_url
  390. result["characters_count"] = data.characters_count if hasattr(data, 'characters_count') else 0
  391. result["page_count"] = data.page_count if hasattr(data, 'page_count') else 0
  392. return result
  393. except Exception as e:
  394. logger.error(f"获取文档翻译结果失败: {str(e)}")
  395. raise Exception(f"获取文档翻译结果失败: {str(e)}")
  396. def submit_html_task(
  397. self,
  398. html_content: str,
  399. target_language: str,
  400. source_language: str = "auto",
  401. scene: str = "mt-turbo",
  402. terminologies: Optional[List[Dict[str, str]]] = None,
  403. domain_hint: Optional[str] = None
  404. ) -> str:
  405. """
  406. 提交HTML翻译任务
  407. Args:
  408. html_content: HTML内容
  409. target_language: 目标语言
  410. source_language: 源语言
  411. scene: 模型类型
  412. terminologies: 术语干预列表
  413. domain_hint: 领域提示
  414. Returns:
  415. 任务ID
  416. """
  417. try:
  418. # 构建基础请求
  419. request = anytrans_models.SubmitHtmlTranslateTaskRequest(
  420. workspace_id=self.workspace_id,
  421. source_language=source_language,
  422. target_language=target_language,
  423. text=html_content,
  424. scene=scene
  425. )
  426. # 添加扩展参数
  427. if terminologies or domain_hint:
  428. ext = anytrans_models.SubmitHtmlTranslateTaskRequestExt()
  429. if terminologies:
  430. term_objects = []
  431. for term in terminologies:
  432. term_obj = anytrans_models.SubmitHtmlTranslateTaskRequestExtTerminologies()
  433. term_obj.src = term["src"]
  434. term_obj.tgt = term["tgt"]
  435. term_objects.append(term_obj)
  436. ext.terminologies = term_objects
  437. if domain_hint:
  438. ext.domain_hint = domain_hint
  439. request.ext = ext
  440. response = self.client.submit_html_translate_task(request)
  441. if response.status_code != 200:
  442. raise Exception(f"API调用失败: {response.status_code}")
  443. return response.body.data.task_id
  444. except Exception as e:
  445. logger.error(f"提交HTML任务失败: {str(e)}")
  446. raise Exception(f"提交HTML任务失败: {str(e)}")
  447. def get_html_result(self, task_id: str) -> Dict[str, Any]:
  448. """
  449. 获取HTML翻译结果
  450. Args:
  451. task_id: 任务ID
  452. Returns:
  453. 任务结果
  454. """
  455. try:
  456. request = anytrans_models.GetHtmlTranslateTaskRequest(
  457. workspace_id=self.workspace_id,
  458. task_id=task_id
  459. )
  460. response = self.client.get_html_translate_task(request)
  461. if response.status_code != 200:
  462. raise Exception(f"API调用失败: {response.status_code}")
  463. body = response.body
  464. # 检查是否有data字段
  465. if not hasattr(body, 'data'):
  466. logger.warning(f"HTML翻译任务 {task_id} 暂无结果数据")
  467. return {
  468. "task_id": task_id,
  469. "status": "processing"
  470. }
  471. data = body.data
  472. # 检查是否有translation字段(翻译完成)
  473. if hasattr(data, 'translation') and data.translation:
  474. result = {
  475. "task_id": task_id,
  476. "status": "completed",
  477. "translated_html": data.translation
  478. }
  479. # 添加token使用统计
  480. if hasattr(data, 'usage'):
  481. result["usage"] = {
  482. "input_tokens": data.usage.input_tokens if hasattr(data.usage, 'input_tokens') else 0,
  483. "output_tokens": data.usage.output_tokens if hasattr(data.usage, 'output_tokens') else 0,
  484. "total_tokens": data.usage.total_tokens if hasattr(data.usage, 'total_tokens') else 0
  485. }
  486. return result
  487. else:
  488. # 任务还在处理中
  489. return {
  490. "task_id": task_id,
  491. "status": "processing"
  492. }
  493. except Exception as e:
  494. error_msg = str(e)
  495. logger.error(f"获取HTML结果失败: {error_msg}")
  496. # 如果是服务内部异常,可能是任务还在处理中
  497. if "Server.Internal.Error" in error_msg or "服务内部异常" in error_msg:
  498. logger.info(f"HTML翻译任务 {task_id} 可能还在处理中")
  499. return {
  500. "task_id": task_id,
  501. "status": "processing"
  502. }
  503. raise Exception(f"获取HTML结果失败: {error_msg}")
  504. def term_query(
  505. self,
  506. source_language: str,
  507. target_language: str,
  508. scene: str = "mt-turbo",
  509. text: Optional[str] = None
  510. ) -> List[Dict[str, str]]:
  511. """
  512. 查询术语库
  513. Args:
  514. source_language: 源语言
  515. target_language: 目标语言
  516. scene: 模型类型
  517. text: 包含术语的句子(可选)
  518. Returns:
  519. 术语列表
  520. """
  521. try:
  522. request = anytrans_models.TermQueryRequest(
  523. workspace_id=self.workspace_id,
  524. source_language=source_language,
  525. target_language=target_language,
  526. scene=scene,
  527. text=text
  528. )
  529. runtime = util_models.RuntimeOptions()
  530. response = self.client.term_query_with_options(request, runtime)
  531. if response.status_code != 200:
  532. raise Exception(f"API调用失败: {response.status_code}")
  533. body = response.body
  534. terms = []
  535. if hasattr(body.data, 'terms'):
  536. for term in body.data.terms:
  537. terms.append({
  538. "term_id": term.term_id,
  539. "src": term.src,
  540. "tgt": term.tgt
  541. })
  542. return terms
  543. except Exception as e:
  544. logger.error(f"查询术语失败: {str(e)}")
  545. raise Exception(f"查询术语失败: {str(e)}")
  546. def term_edit(
  547. self,
  548. action: str,
  549. source_language: str,
  550. target_language: str,
  551. scene: str,
  552. terms: List[Dict[str, str]]
  553. ) -> List[Dict[str, str]]:
  554. """
  555. 编辑术语库
  556. Args:
  557. action: 操作类型(ADD/DELETE/MODIFY)
  558. source_language: 源语言
  559. target_language: 目标语言
  560. scene: 模型类型
  561. terms: 术语列表 [{"src": "源文本", "tgt": "目标文本", "term_id": "术语ID"}]
  562. Returns:
  563. 操作后的术语列表
  564. """
  565. try:
  566. # 构建术语对象
  567. term_objects = []
  568. for term in terms:
  569. term_obj = anytrans_models.TermEditRequestTerms()
  570. term_obj.src = term["src"]
  571. term_obj.tgt = term["tgt"]
  572. if "term_id" in term:
  573. term_obj.term_id = term["term_id"]
  574. term_objects.append(term_obj)
  575. request = anytrans_models.TermEditRequest(
  576. workspace_id=self.workspace_id,
  577. action=action,
  578. source_language=source_language,
  579. target_language=target_language,
  580. scene=scene,
  581. terms=term_objects
  582. )
  583. runtime = util_models.RuntimeOptions()
  584. response = self.client.term_edit_with_options(request, runtime)
  585. if response.status_code != 200:
  586. raise Exception(f"API调用失败: {response.status_code}")
  587. body = response.body
  588. result_terms = []
  589. if hasattr(body.data, 'terms'):
  590. for term in body.data.terms:
  591. result_terms.append({
  592. "term_id": term.term_id,
  593. "src": term.src,
  594. "tgt": term.tgt
  595. })
  596. return result_terms
  597. except Exception as e:
  598. logger.error(f"编辑术语失败: {str(e)}")
  599. raise Exception(f"编辑术语失败: {str(e)}")