ImageService.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. <?php
  2. namespace app\service;
  3. use app\service\AIGatewayService;
  4. use think\Db;
  5. use think\Exception;
  6. use think\Queue;
  7. /**
  8. * ImageService 图像任务服务
  9. * 负责图生文、文生文、文生图、图生图等 AI 任务的队列派发与状态管理
  10. */
  11. class ImageService
  12. {
  13. /** AI 异步任务状态(Redis / GetImageStatus 返回中文) */
  14. public const TASK_STATUS_PENDING = '排队中';
  15. public const TASK_STATUS_PROCESSING = '生成中';
  16. public const TASK_STATUS_COMPLETED = '已完成';
  17. public const TASK_STATUS_FAILED = '失败';
  18. /** 队列名称 */
  19. private const QUEUE_ARRIMAGE = 'arrimage';
  20. /** Redis 任务状态 TTL(秒) */
  21. private const TASK_TTL = 300;
  22. /** Redis key 前缀 */
  23. private const KEY_TEXT_TO_IMAGE = 'text_to_image_task:';
  24. private const KEY_IMG_TO_IMG = 'img_to_img_task:';
  25. /** 是否已完成(兼容旧英文状态) */
  26. public static function isTaskCompleted(string $status): bool
  27. {
  28. return in_array($status, [self::TASK_STATUS_COMPLETED, 'completed'], true);
  29. }
  30. /** 英文状态转中文(兼容 Redis 里尚未过期的旧任务) */
  31. public static function normalizeTaskStatus(string $status): string
  32. {
  33. $map = [
  34. 'pending' => self::TASK_STATUS_PENDING,
  35. 'processing' => self::TASK_STATUS_PROCESSING,
  36. 'completed' => self::TASK_STATUS_COMPLETED,
  37. 'failed' => self::TASK_STATUS_FAILED,
  38. ];
  39. return $map[$status] ?? $status;
  40. }
  41. /**
  42. * 图生文:提交到队列
  43. * @param array $params 请求参数
  44. * @return bool
  45. */
  46. public function handleImgToText(array $params): bool
  47. {
  48. Queue::push('app\job\ImageArrJob', $params, self::QUEUE_ARRIMAGE);
  49. return true;
  50. }
  51. /**
  52. * 文生文:直接调用 API 并返回结果
  53. * @param string $prompt 提示词
  54. * @param string $model 模型名称
  55. * @return array ['success'=>bool, 'message'=>string, 'data'=>string]
  56. */
  57. public function handleTextToText($status_val, string $prompt, string $model): array
  58. {
  59. $gptRes = (new AIGatewayService())->buildRequestData($status_val, $model, $prompt);
  60. if (isset($gptRes['error'])) {
  61. $err = $gptRes['error'];
  62. $msg = is_array($err)
  63. ? ($err['message'] ?? json_encode($err, JSON_UNESCAPED_UNICODE))
  64. : (string)$err;
  65. return ['success' => false, 'message' => $msg ?: '生成失败', 'data' => ''];
  66. }
  67. $gptText = $this->extractTextFromAiResponse($gptRes);
  68. if ($gptText === '') {
  69. return ['success' => false, 'message' => '模型未返回文本内容', 'data' => ''];
  70. }
  71. return ['success' => true, 'message' => '生成成功', 'data' => $gptText];
  72. }
  73. /**
  74. * 从 AI 响应中提取文本(兼容 Gemini / OpenAI 格式)
  75. */
  76. private function extractTextFromAiResponse(array $response): string
  77. {
  78. if (isset($response['candidates'][0]['content']['parts'][0]['text'])) {
  79. return trim((string)$response['candidates'][0]['content']['parts'][0]['text']);
  80. }
  81. $content = $response['choices'][0]['message']['content'] ?? null;
  82. if (is_string($content)) {
  83. return trim($content);
  84. }
  85. if (is_array($content)) {
  86. foreach ($content as $part) {
  87. if (is_array($part) && isset($part['text']) && $part['text'] !== '') {
  88. return trim((string)$part['text']);
  89. }
  90. }
  91. }
  92. return trim((string)($response['output_text'] ?? $response['text'] ?? ''));
  93. }
  94. /**
  95. * 文生图:创建任务并推送到队列
  96. * @param array $params 含 id、model 等
  97. * @return array ['success'=>bool, 'message'=>string, 'task_id'=>string]
  98. */
  99. public function handleTextToImg(array $params): array
  100. {
  101. return $this->submitTaskToQueue(
  102. $params,
  103. self::KEY_TEXT_TO_IMAGE,
  104. '正在生成图片中,请稍等.....'
  105. );
  106. }
  107. /**
  108. * 图生图:创建任务并推送到队列(产品图+模板图)
  109. * @param array $params 含 id、product_img、template_img、prompt、model 等
  110. * @return array ['success'=>bool, 'message'=>string, 'task_id'=>string]
  111. */
  112. public function handleImgToImg(array $params): array
  113. {
  114. return $this->submitTaskToQueue(
  115. $params,
  116. self::KEY_IMG_TO_IMG,
  117. '正在生成图片中,请稍等.....'
  118. );
  119. }
  120. /**
  121. * 通用:创建任务 ID、写入 Redis、推送到队列
  122. */
  123. private function submitTaskToQueue(array $params, string $redisKeyPrefix, string $message): array
  124. {
  125. $taskId = ($params['id'] ?? '0') . '-' . date('YmdHis') . '-' . mt_rand(1000, 9999);
  126. $params['task_id'] = $taskId;
  127. $redis = getTaskRedis();
  128. $redis->set($redisKeyPrefix . $taskId, json_encode([
  129. 'status' => self::TASK_STATUS_PENDING,
  130. 'created_at' => date('Y-m-d H:i:s')
  131. ]), ['EX' => self::TASK_TTL]);
  132. Queue::push('app\job\ImageArrJob', $params, self::QUEUE_ARRIMAGE);
  133. return [
  134. 'success' => true,
  135. 'message' => $message,
  136. 'task_id' => $taskId
  137. ];
  138. }
  139. /**
  140. * 批量图像任务:支持链式任务和单类型任务
  141. * @param array $params 含 batch、num、type、old_image_file 等
  142. * @return bool
  143. */
  144. public function handleImage(array $params): bool
  145. {
  146. if (!isset($params['batch']) || !is_array($params['batch'])) {
  147. return false;
  148. }
  149. $arr = $this->buildBatchItems($params);
  150. $insertData = $this->buildQueueLogData($params, count($arr));
  151. if (empty($params['type'])) {
  152. $this->dispatchFullChainTask($arr, $insertData);
  153. } else {
  154. $result = $this->dispatchSingleTypeTask($arr, $params, $insertData);
  155. if (!$result) {
  156. return false;
  157. }
  158. }
  159. return true;
  160. }
  161. /** 构建批量任务项 */
  162. private function buildBatchItems(array $params): array
  163. {
  164. $arr = [];
  165. foreach ($params['batch'] as $v) {
  166. $baseItem = [
  167. 'sourceDir' => $this->sourceDir($v, 1),
  168. 'outputDir' => $this->sourceDir($v, 2),
  169. 'file_name' => $this->sourceDir($v, 3),
  170. 'type' => $params['type'] ?? '',
  171. 'selectedOption' => $params['selectedOption'] ?? '',
  172. 'txttotxt_selectedOption' => $params['txttotxt_selectedOption'] ?? '',
  173. 'imgtotxt_selectedOption' => $params['imgtotxt_selectedOption'] ?? '',
  174. 'prompt' => '',
  175. 'width' => $params['width'] ?? 0,
  176. 'height' => $params['height'] ?? 0,
  177. 'executeKeywords' => $params['executeKeywords'] ?? '',
  178. 'sys_id' => $params['sys_id'] ?? ''
  179. ];
  180. $num = (int)($params['num'] ?? 1);
  181. $arr = array_merge($arr, array_fill(0, max(1, $num), $baseItem));
  182. }
  183. return $arr;
  184. }
  185. /** 构建队列日志插入数据 */
  186. private function buildQueueLogData(array $params, int $imageCount): array
  187. {
  188. return [
  189. 'create_time' => date('Y-m-d H:i:s'),
  190. 'old_image_file' => $params['old_image_file'] ?? '',
  191. 'status' => '等待中',
  192. 'image_count' => $imageCount,
  193. 'params' => json_encode($params, JSON_UNESCAPED_UNICODE)
  194. ];
  195. }
  196. /** 派发一键链式任务:图生文→文生文→文生图→图生图→高清放大 */
  197. private function dispatchFullChainTask(array $arr, array $insertData): void
  198. {
  199. $params = json_decode($insertData['params'], true) ?: [];
  200. $insertData['model'] = 'gpt-4-vision-preview,gpt-4,' . ($params['selectedOption'] ?? '');
  201. $insertData['model_name'] = '文生图';
  202. $taskId = Db::name('queue_logs')->insertGetId($insertData);
  203. $arr = array_map(function ($item) use ($taskId) {
  204. $item['type'] = '图生文';
  205. $item['chain_next'] = ['文生文', '文生图', '图生图', '高清放大'];
  206. $item['task_id'] = $taskId;
  207. return $item;
  208. }, $arr);
  209. Queue::push('app\job\ImageArrJob', ['task_id' => $taskId, 'data' => $arr], self::QUEUE_ARRIMAGE);
  210. }
  211. /** 派发单类型任务 */
  212. private function dispatchSingleTypeTask(array $arr, array $params, array $insertData): bool
  213. {
  214. $typeConfig = $this->getTypeConfig($params['type'], $params);
  215. if (!$typeConfig) {
  216. return false;
  217. }
  218. $insertData['model'] = $typeConfig['model'];
  219. $insertData['model_name'] = $typeConfig['model_name'];
  220. $taskId = Db::name('queue_logs')->insertGetId($insertData);
  221. $arr = array_map(function ($item) use ($params, $taskId) {
  222. $item['type'] = $params['type'];
  223. $item['task_id'] = $taskId;
  224. return $item;
  225. }, $arr);
  226. Queue::push('app\job\ImageArrJob', ['task_id' => $taskId, 'data' => $arr], self::QUEUE_ARRIMAGE);
  227. return true;
  228. }
  229. /** 任务类型对应的 model 配置 */
  230. private function getTypeConfig(string $type, array $params = []): ?array
  231. {
  232. $configs = [
  233. '图生文' => ['model' => 'gpt-4-vision-preview', 'model_name' => '图生文'],
  234. '文生文' => ['model_key' => 'txttotxt_selectedOption', 'model_name' => '文生文'],
  235. '文生图' => ['model_key' => 'selectedOption', 'model_name' => '文生图'],
  236. '图生图' => ['model' => 'realisticVisionV51_v51VAE-inpainting.safetensors [f0d4872d24]', 'model_name' => '图生图'],
  237. '高清放大' => ['model' => '高清放大', 'model_name' => '高清放大']
  238. ];
  239. $cfg = $configs[$type] ?? null;
  240. if (!$cfg) {
  241. return null;
  242. }
  243. if (isset($cfg['model_key'])) {
  244. $cfg['model'] = $params[$cfg['model_key']] ?? '';
  245. unset($cfg['model_key']);
  246. }
  247. return $cfg;
  248. }
  249. /**
  250. * 解析图像路径
  251. * @param string $filePath 如 uploads/operate/ai/Preview/20240610/xxx.png
  252. * @param int $type 1=源目录 2=输出目录 3=文件名
  253. * @return string|null
  254. */
  255. public function sourceDir(string $filePath, int $type): ?string
  256. {
  257. $pathParts = explode('/', $filePath);
  258. $filename = array_pop($pathParts);
  259. $baseParts = $pathParts;
  260. $date = '';
  261. foreach ($pathParts as $index => $part) {
  262. if (preg_match('/^\d{8}$/', $part)) {
  263. $date = $part;
  264. unset($baseParts[$index]);
  265. break;
  266. }
  267. }
  268. $basePath = implode('/', $baseParts);
  269. switch ($type) {
  270. case 1:
  271. return $basePath;
  272. case 2:
  273. return '/' . str_replace('/Preview/', '/dall-e/', $basePath) . $date;
  274. case 3:
  275. return $filename;
  276. default:
  277. return null;
  278. }
  279. }
  280. }