TranslateService.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. <?php
  2. namespace App\Services\AIAssistant;
  3. use App\Services\NissayaParser;
  4. use App\Services\OpenAIService;
  5. use App\Services\RomanizeService;
  6. use App\Services\AIModelService;
  7. use App\Http\Controllers\AuthController;
  8. use Illuminate\Support\Facades\Log;
  9. use App\Http\Resources\AiModelResource;
  10. use App\DTO\LLMTranslation\TranslationResponseDTO;
  11. class TranslateService
  12. {
  13. protected OpenAIService $openAIService;
  14. protected NissayaParser $nissayaParser;
  15. protected RomanizeService $romanizeService;
  16. protected AIModelService $aiModelService;
  17. protected AiModelResource $model;
  18. protected string $modelToken;
  19. protected bool $stream = false;
  20. protected array $original; //需要被翻译的原文
  21. protected string $systemPrompt = '';
  22. /**
  23. * 翻译提示词模板
  24. */
  25. protected string $translatePrompt = '';
  26. public function __construct(
  27. OpenAIService $openAIService,
  28. AIModelService $aiModelService,
  29. ) {
  30. $this->openAIService = $openAIService;
  31. $this->aiModelService = $aiModelService;
  32. }
  33. /**
  34. * 设置模型配置
  35. *
  36. * @param string $model
  37. * @return self
  38. */
  39. public function setModel(string $model): self
  40. {
  41. $this->model = $this->aiModelService->getModelById($model);
  42. $this->modelToken = AuthController::getUserToken($model);
  43. return $this;
  44. }
  45. /**
  46. * 设置翻译提示词
  47. *
  48. * @param string $prompt
  49. * @return self
  50. */
  51. public function setSystemPrompt(string $prompt): self
  52. {
  53. $this->systemPrompt = $prompt;
  54. return $this;
  55. }
  56. /**
  57. * 设置翻译提示词
  58. *
  59. * @param string $prompt
  60. * @return self
  61. */
  62. public function setTranslatePrompt(string $prompt): self
  63. {
  64. $this->translatePrompt = $prompt;
  65. return $this;
  66. }
  67. /**
  68. * 翻译缅文版逐词解析
  69. *
  70. * @param string $text 格式: 巴利文=缅文
  71. * @param bool $stream 是否流式输出
  72. * @return TranslationResponseDTO
  73. * @throws \Exception
  74. */
  75. public function translate(): TranslationResponseDTO
  76. {
  77. $startAt = time();
  78. try {
  79. Log::info('准备翻译', [
  80. 'systemPrompt' => $this->systemPrompt,
  81. 'translatePrompt' => $this->translatePrompt,
  82. ]);
  83. // 3. 调用LLM进行翻译
  84. $response = $this->openAIService
  85. ->setApiUrl($this->model['url'])
  86. ->setModel($this->model['model'])
  87. ->setApiKey($this->model['key'])
  88. ->setSystemPrompt($this->systemPrompt)
  89. ->setTemperature(0.3)
  90. ->setStream($this->stream)
  91. ->send($this->translatePrompt);
  92. $complete = time() - $startAt;
  93. $content = $response['choices'][0]['message']['content'] ?? '';
  94. if (empty($content)) {
  95. throw new \Exception('LLM返回内容为空');
  96. }
  97. Log::info('翻译完成', [
  98. 'content'=>$content,
  99. 'duration' => $complete,
  100. 'input_tokens' => $response['usage']['prompt_tokens'] ?? 0,
  101. 'output_tokens' => $response['usage']['completion_tokens'] ?? 0,
  102. ]);
  103. // 4. 解析JSONL格式的翻译结果
  104. $translatedData = $this->jsonlToArray($content);
  105. Log::info('解析完成', [
  106. 'output_items' => count($translatedData),
  107. ]);
  108. return TranslationResponseDTO::fromArray([
  109. 'success' => true,
  110. 'data' => $translatedData,
  111. 'meta' => [
  112. 'duration' => $complete,
  113. 'items_count' => count($translatedData),
  114. 'usage' => $response['usage'] ?? [],
  115. ],
  116. ]);
  117. } catch (\Exception $e) {
  118. Log::error('NissayaTranslate: 翻译失败', [
  119. 'error' => $e->getMessage(),
  120. 'trace' => $e->getTraceAsString(),
  121. ]);
  122. return TranslationResponseDTO::fromArray([
  123. 'success' => false,
  124. 'error' => $e->getMessage(),
  125. 'data' => [],
  126. ]);
  127. }
  128. }
  129. /**
  130. * 将数组转换为JSONL格式
  131. *
  132. * @param array $data
  133. * @return string
  134. */
  135. protected function arrayToJsonl(array $data): string
  136. {
  137. $lines = [];
  138. foreach ($data as $item) {
  139. $lines[] = json_encode($item, JSON_UNESCAPED_UNICODE);
  140. }
  141. return implode("\n", $lines);
  142. }
  143. /**
  144. * 将JSONL格式转换为数组
  145. *
  146. * @param string $jsonl
  147. * @return array
  148. */
  149. protected function jsonlToArray(string $jsonl): array
  150. {
  151. // 清理可能的markdown代码块标记
  152. $jsonl = preg_replace('/```json\s*|\s*```/', '', $jsonl);
  153. $jsonl = trim($jsonl);
  154. $lines = explode("\n", $jsonl);
  155. $result = [];
  156. foreach ($lines as $line) {
  157. $line = trim($line);
  158. if (empty($line)) {
  159. continue;
  160. }
  161. $decoded = json_decode($line, true);
  162. if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
  163. $result[] = $decoded;
  164. } else {
  165. Log::warning('无法解析JSON行', [
  166. 'line' => $line,
  167. 'error' => json_last_error_msg(),
  168. ]);
  169. }
  170. }
  171. return $result;
  172. }
  173. /**
  174. * 批量翻译(将大文本分批处理)
  175. *
  176. * @param string $text
  177. * @param int $batchSize 每批处理的条目数
  178. * @return array
  179. */
  180. public function translateInBatches(string $text, int $batchSize = 50): array
  181. {
  182. try {
  183. $parsedData = $this->nissayaParser->parse($text);
  184. $batches = array_chunk($parsedData, $batchSize);
  185. $allResults = [];
  186. $totalDuration = 0;
  187. $totalUsage = [
  188. 'prompt_tokens' => 0,
  189. 'completion_tokens' => 0,
  190. 'total_tokens' => 0,
  191. ];
  192. foreach ($batches as $index => $batch) {
  193. Log::info("NissayaTranslate: 处理批次 " . ($index + 1) . "/" . count($batches));
  194. $jsonlInput = $this->arrayToJsonl($batch);
  195. $response = $this->openAIService
  196. ->setApiUrl($this->model['url'])
  197. ->setModel($this->model['model'])
  198. ->setApiKey($this->model['key'])
  199. ->setSystemPrompt($this->translatePrompt)
  200. ->setTemperature(0.7)
  201. ->setStream(false)
  202. ->send($jsonlInput);
  203. $content = $response['choices'][0]['message']['content'] ?? '';
  204. $translatedBatch = $this->jsonlToArray($content);
  205. $allResults = array_merge($allResults, $translatedBatch);
  206. // 累计使用统计
  207. if (isset($response['usage'])) {
  208. $totalUsage['prompt_tokens'] += $response['usage']['prompt_tokens'] ?? 0;
  209. $totalUsage['completion_tokens'] += $response['usage']['completion_tokens'] ?? 0;
  210. $totalUsage['total_tokens'] += $response['usage']['total_tokens'] ?? 0;
  211. }
  212. }
  213. return [
  214. 'success' => true,
  215. 'data' => $allResults,
  216. 'meta' => [
  217. 'batches' => count($batches),
  218. 'items_count' => count($allResults),
  219. 'usage' => $totalUsage,
  220. ],
  221. ];
  222. } catch (\Exception $e) {
  223. Log::error('NissayaTranslate: 批量翻译失败', [
  224. 'error' => $e->getMessage(),
  225. ]);
  226. return [
  227. 'success' => false,
  228. 'error' => $e->getMessage(),
  229. 'data' => [],
  230. ];
  231. }
  232. }
  233. }