ImageService.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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. $ai = new AIGatewayService();
  60. $gptRes = $ai->buildRequestData($status_val, $model, $prompt);
  61. $gptText = '';
  62. if (isset($gptRes['candidates'][0]['content']['parts'][0]['text'])) {
  63. $gptText = $gptRes['candidates'][0]['content']['parts'][0]['text'];
  64. } elseif (isset($gptRes['choices'][0]['message']['content'])) {
  65. $gptText = trim($gptRes['choices'][0]['message']['content']);
  66. }
  67. if (isset($gptRes['error']) || isset($gptRes['code']) && $gptRes['code'] !== 0) {
  68. return [
  69. 'success' => false,
  70. 'message' => $gptRes['msg'] ?? $gptRes['error']['message'] ?? '生成失败',
  71. 'data' => ''
  72. ];
  73. }
  74. return [
  75. 'success' => true,
  76. 'message' => '生成成功',
  77. 'data' => $gptText
  78. ];
  79. }
  80. /**
  81. * 文生图:创建任务并推送到队列
  82. * @param array $params 含 id、model 等
  83. * @return array ['success'=>bool, 'message'=>string, 'task_id'=>string]
  84. */
  85. public function handleTextToImg(array $params): array
  86. {
  87. return $this->submitTaskToQueue(
  88. $params,
  89. self::KEY_TEXT_TO_IMAGE,
  90. '正在生成图片中,请稍等.....'
  91. );
  92. }
  93. /**
  94. * 图生图:创建任务并推送到队列(产品图+模板图)
  95. * @param array $params 含 id、product_img、template_img、prompt、model 等
  96. * @return array ['success'=>bool, 'message'=>string, 'task_id'=>string]
  97. */
  98. public function handleImgToImg(array $params): array
  99. {
  100. return $this->submitTaskToQueue(
  101. $params,
  102. self::KEY_IMG_TO_IMG,
  103. '正在生成图片中,请稍等.....'
  104. );
  105. }
  106. /**
  107. * 通用:创建任务 ID、写入 Redis、推送到队列
  108. */
  109. private function submitTaskToQueue(array $params, string $redisKeyPrefix, string $message): array
  110. {
  111. $taskId = ($params['id'] ?? '0') . '-' . date('YmdHis') . '-' . mt_rand(1000, 9999);
  112. $params['task_id'] = $taskId;
  113. $redis = getTaskRedis();
  114. $redis->set($redisKeyPrefix . $taskId, json_encode([
  115. 'status' => self::TASK_STATUS_PENDING,
  116. 'created_at' => date('Y-m-d H:i:s')
  117. ]), ['EX' => self::TASK_TTL]);
  118. Queue::push('app\job\ImageArrJob', $params, self::QUEUE_ARRIMAGE);
  119. return [
  120. 'success' => true,
  121. 'message' => $message,
  122. 'task_id' => $taskId
  123. ];
  124. }
  125. /**
  126. * 批量图像任务:支持链式任务和单类型任务
  127. * @param array $params 含 batch、num、type、old_image_file 等
  128. * @return bool
  129. */
  130. public function handleImage(array $params): bool
  131. {
  132. if (!isset($params['batch']) || !is_array($params['batch'])) {
  133. return false;
  134. }
  135. $arr = $this->buildBatchItems($params);
  136. $insertData = $this->buildQueueLogData($params, count($arr));
  137. if (empty($params['type'])) {
  138. $this->dispatchFullChainTask($arr, $insertData);
  139. } else {
  140. $result = $this->dispatchSingleTypeTask($arr, $params, $insertData);
  141. if (!$result) {
  142. return false;
  143. }
  144. }
  145. return true;
  146. }
  147. /** 构建批量任务项 */
  148. private function buildBatchItems(array $params): array
  149. {
  150. $arr = [];
  151. foreach ($params['batch'] as $v) {
  152. $baseItem = [
  153. 'sourceDir' => $this->sourceDir($v, 1),
  154. 'outputDir' => $this->sourceDir($v, 2),
  155. 'file_name' => $this->sourceDir($v, 3),
  156. 'type' => $params['type'] ?? '',
  157. 'selectedOption' => $params['selectedOption'] ?? '',
  158. 'txttotxt_selectedOption' => $params['txttotxt_selectedOption'] ?? '',
  159. 'imgtotxt_selectedOption' => $params['imgtotxt_selectedOption'] ?? '',
  160. 'prompt' => '',
  161. 'width' => $params['width'] ?? 0,
  162. 'height' => $params['height'] ?? 0,
  163. 'executeKeywords' => $params['executeKeywords'] ?? '',
  164. 'sys_id' => $params['sys_id'] ?? ''
  165. ];
  166. $num = (int)($params['num'] ?? 1);
  167. $arr = array_merge($arr, array_fill(0, max(1, $num), $baseItem));
  168. }
  169. return $arr;
  170. }
  171. /** 构建队列日志插入数据 */
  172. private function buildQueueLogData(array $params, int $imageCount): array
  173. {
  174. return [
  175. 'create_time' => date('Y-m-d H:i:s'),
  176. 'old_image_file' => $params['old_image_file'] ?? '',
  177. 'status' => '等待中',
  178. 'image_count' => $imageCount,
  179. 'params' => json_encode($params, JSON_UNESCAPED_UNICODE)
  180. ];
  181. }
  182. /** 派发一键链式任务:图生文→文生文→文生图→图生图→高清放大 */
  183. private function dispatchFullChainTask(array $arr, array $insertData): void
  184. {
  185. $params = json_decode($insertData['params'], true) ?: [];
  186. $insertData['model'] = 'gpt-4-vision-preview,gpt-4,' . ($params['selectedOption'] ?? '');
  187. $insertData['model_name'] = '文生图';
  188. $taskId = Db::name('queue_logs')->insertGetId($insertData);
  189. $arr = array_map(function ($item) use ($taskId) {
  190. $item['type'] = '图生文';
  191. $item['chain_next'] = ['文生文', '文生图', '图生图', '高清放大'];
  192. $item['task_id'] = $taskId;
  193. return $item;
  194. }, $arr);
  195. Queue::push('app\job\ImageArrJob', ['task_id' => $taskId, 'data' => $arr], self::QUEUE_ARRIMAGE);
  196. }
  197. /** 派发单类型任务 */
  198. private function dispatchSingleTypeTask(array $arr, array $params, array $insertData): bool
  199. {
  200. $typeConfig = $this->getTypeConfig($params['type'], $params);
  201. if (!$typeConfig) {
  202. return false;
  203. }
  204. $insertData['model'] = $typeConfig['model'];
  205. $insertData['model_name'] = $typeConfig['model_name'];
  206. $taskId = Db::name('queue_logs')->insertGetId($insertData);
  207. $arr = array_map(function ($item) use ($params, $taskId) {
  208. $item['type'] = $params['type'];
  209. $item['task_id'] = $taskId;
  210. return $item;
  211. }, $arr);
  212. Queue::push('app\job\ImageArrJob', ['task_id' => $taskId, 'data' => $arr], self::QUEUE_ARRIMAGE);
  213. return true;
  214. }
  215. /** 任务类型对应的 model 配置 */
  216. private function getTypeConfig(string $type, array $params = []): ?array
  217. {
  218. $configs = [
  219. '图生文' => ['model' => 'gpt-4-vision-preview', 'model_name' => '图生文'],
  220. '文生文' => ['model_key' => 'txttotxt_selectedOption', 'model_name' => '文生文'],
  221. '文生图' => ['model_key' => 'selectedOption', 'model_name' => '文生图'],
  222. '图生图' => ['model' => 'realisticVisionV51_v51VAE-inpainting.safetensors [f0d4872d24]', 'model_name' => '图生图'],
  223. '高清放大' => ['model' => '高清放大', 'model_name' => '高清放大']
  224. ];
  225. $cfg = $configs[$type] ?? null;
  226. if (!$cfg) {
  227. return null;
  228. }
  229. if (isset($cfg['model_key'])) {
  230. $cfg['model'] = $params[$cfg['model_key']] ?? '';
  231. unset($cfg['model_key']);
  232. }
  233. return $cfg;
  234. }
  235. /**
  236. * 解析图像路径
  237. * @param string $filePath 如 uploads/operate/ai/Preview/20240610/xxx.png
  238. * @param int $type 1=源目录 2=输出目录 3=文件名
  239. * @return string|null
  240. */
  241. public function sourceDir(string $filePath, int $type): ?string
  242. {
  243. $pathParts = explode('/', $filePath);
  244. $filename = array_pop($pathParts);
  245. $baseParts = $pathParts;
  246. $date = '';
  247. foreach ($pathParts as $index => $part) {
  248. if (preg_match('/^\d{8}$/', $part)) {
  249. $date = $part;
  250. unset($baseParts[$index]);
  251. break;
  252. }
  253. }
  254. $basePath = implode('/', $baseParts);
  255. switch ($type) {
  256. case 1:
  257. return $basePath;
  258. case 2:
  259. return '/' . str_replace('/Preview/', '/dall-e/', $basePath) . $date;
  260. case 3:
  261. return $filename;
  262. default:
  263. return null;
  264. }
  265. }
  266. }