|
|
@@ -18,6 +18,8 @@ class AIImageGenerationService
|
|
|
|
|
|
// 模型常量
|
|
|
const MODEL_JIMENG_4 = 'jimeng_4.0';
|
|
|
+ const MODEL_NANO_BANANA_2 = 'NanoBanana2';
|
|
|
+ const MODEL_NANO_BANANA_PRO = 'NanoBananaPro';
|
|
|
|
|
|
public function __construct(VolcEngineService $volcEngineService)
|
|
|
{
|
|
|
@@ -66,9 +68,11 @@ class AIImageGenerationService
|
|
|
'model' => $model,
|
|
|
]);
|
|
|
|
|
|
- // 仅即梦AI立即提交任务,火山API在定时任务中提交
|
|
|
+ // 即梦AI和NanoBanana系列立即提交任务,火山API在定时任务中提交
|
|
|
if ($model === self::MODEL_JIMENG_4) {
|
|
|
$this->submitTaskToJimengApi($task);
|
|
|
+ } elseif (in_array($model, BaseConst::NANO_BANANA_MODELS)) {
|
|
|
+ $this->submitTaskToNanoBananaApi($task);
|
|
|
}
|
|
|
|
|
|
return $task;
|
|
|
@@ -563,6 +567,8 @@ class AIImageGenerationService
|
|
|
// 根据模型类型调用不同的查询方法
|
|
|
if ($task->model === self::MODEL_JIMENG_4) {
|
|
|
return $this->queryJimengTaskStatus($task);
|
|
|
+ } elseif (in_array($task->model, BaseConst::NANO_BANANA_MODELS)) {
|
|
|
+ return $this->queryNanoBananaTaskStatus($task);
|
|
|
} else {
|
|
|
// 火山API是同步返回结果,不需要查询
|
|
|
return [
|
|
|
@@ -683,6 +689,228 @@ class AIImageGenerationService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 提交任务到NanoBanana API
|
|
|
+ *
|
|
|
+ * @param MpGeneratePicTask $task
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ private function submitTaskToNanoBananaApi(MpGeneratePicTask $task): void
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ // 验证环境变量配置
|
|
|
+ $apiKey = env('NANO_BANANA_API_KEY');
|
|
|
+
|
|
|
+ if (empty($apiKey)) {
|
|
|
+ $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
|
|
|
+ 'error_message' => 'NanoBanana API KEY未配置,请检查环境变量NANO_BANANA_API_KEY'
|
|
|
+ ]);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据模型选择API地址
|
|
|
+ switch ($task->model) {
|
|
|
+ case self::MODEL_NANO_BANANA_2:
|
|
|
+ $apiUrl = 'https://api.wuyinkeji.com/api/async/image_nanoBanana2';
|
|
|
+ break;
|
|
|
+ case self::MODEL_NANO_BANANA_PRO:
|
|
|
+ $apiUrl = 'https://api.wuyinkeji.com/api/async/image_nanoBanana_pro';
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
|
|
|
+ 'error_message' => '不支持的NanoBanana模型类型: ' . $task->model
|
|
|
+ ]);
|
|
|
+ logDB('generate', 'error', 'NanoBanana模型类型错误', ['task_id' => $task->task_id, 'model' => $task->model]);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建API请求参数
|
|
|
+ $apiParams = [
|
|
|
+ 'key' => $apiKey,
|
|
|
+ 'prompt' => $task->prompt,
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 尺寸参数(根据宽高判断)
|
|
|
+ $area = $task->width * $task->height;
|
|
|
+ if ($area >= 1024*1024 && $area < 2048*2048) {
|
|
|
+ $apiParams['size'] = '1k';
|
|
|
+ } elseif ($area >= 2048*2048) {
|
|
|
+ $apiParams['size'] = '2k';
|
|
|
+ } else {
|
|
|
+ $apiParams['size'] = '1k'; // 默认1k
|
|
|
+ }
|
|
|
+
|
|
|
+ // 画面比例
|
|
|
+ if ($task->width && $task->height) {
|
|
|
+ $ratio = $task->width . ':' . $task->height;
|
|
|
+ // 简化比例
|
|
|
+ $gcd = $this->gcd($task->width, $task->height);
|
|
|
+ $ratio = ($task->width / $gcd) . ':' . ($task->height / $gcd);
|
|
|
+ $apiParams['aspectRatio'] = $ratio;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 参考图片
|
|
|
+ if ($task->ref_img_url) {
|
|
|
+ $apiParams['urls'] = is_array($task->ref_img_url) ? $task->ref_img_url : [$task->ref_img_url];
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('generate')->info('NanoBanana API参数: ', $apiParams);
|
|
|
+ logDB('generate', 'info', 'NanoBanana任务提交', ['task_id' => $task->task_id, 'model' => $task->model, 'params' => $apiParams]);
|
|
|
+
|
|
|
+ // 调用NanoBanana API
|
|
|
+ $response = $this->httpClient->post($apiUrl, [
|
|
|
+ 'query' => ['key' => $apiKey],
|
|
|
+ 'json' => $apiParams,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $responseData = json_decode($response->getBody()->getContents(), true);
|
|
|
+ dLog('generate')->info('NanoBanana API响应: ', $responseData);
|
|
|
+ logDB('generate', 'info', 'NanoBanana API响应', ['task_id' => $task->task_id, 'response' => $responseData]);
|
|
|
+
|
|
|
+ if ($responseData['code'] !== 200) {
|
|
|
+ // API返回错误
|
|
|
+ logDB('generate', 'error', 'NanoBanana任务提交失败', ['task_id' => $task->task_id, 'error' => $responseData['msg'] ?? 'API Error']);
|
|
|
+ $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
|
|
|
+ 'error_message' => $responseData['msg'] ?? 'API Error',
|
|
|
+ 'extra_params' => $responseData
|
|
|
+ ]);
|
|
|
+ } else {
|
|
|
+ // 任务提交成功,更新状态为处理中
|
|
|
+ $updateData = [];
|
|
|
+ // 存储API返回的任务ID
|
|
|
+ if (isset($responseData['data']['id'])) {
|
|
|
+ $updateData['task_id'] = $responseData['data']['id'];
|
|
|
+ $updateData['extra_params'] = $responseData;
|
|
|
+ }
|
|
|
+
|
|
|
+ $task->updateStatus(MpGeneratePicTask::STATUS_PROCESSING, $updateData);
|
|
|
+ }
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ // 记录错误
|
|
|
+ logDB('generate', 'error', 'NanoBanana任务提交异常', ['task_id' => $task->task_id, 'error' => $e->getMessage()]);
|
|
|
+ $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
|
|
|
+ 'error_message' => 'API请求失败: ' . $e->getMessage()
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询NanoBanana任务状态
|
|
|
+ *
|
|
|
+ * @param MpGeneratePicTask $task
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ private function queryNanoBananaTaskStatus(MpGeneratePicTask $task): array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ // 获取API任务ID
|
|
|
+ $apiTaskId = $task->task_id ?? null;
|
|
|
+ $apiKey = env('NANO_BANANA_API_KEY');
|
|
|
+
|
|
|
+ if (!$apiTaskId) {
|
|
|
+ return [
|
|
|
+ 'status' => 'failed',
|
|
|
+ 'error_message' => 'API任务ID不存在',
|
|
|
+ 'result_url' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (empty($apiKey)) {
|
|
|
+ return [
|
|
|
+ 'status' => 'failed',
|
|
|
+ 'error_message' => 'NanoBanana API KEY未配置',
|
|
|
+ 'result_url' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用统一查询接口
|
|
|
+ $response = $this->httpClient->get('https://api.wuyinkeji.com/api/async/detail', [
|
|
|
+ 'query' => [
|
|
|
+ 'key' => $apiKey,
|
|
|
+ 'id' => $apiTaskId
|
|
|
+ ]
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $responseData = json_decode($response->getBody()->getContents(), true);
|
|
|
+ dLog('generate')->info('NanoBanana任务状态查询响应: ', $responseData);
|
|
|
+ logDB('generate', 'info', 'NanoBanana任务状态查询', ['task_id' => $apiTaskId, 'response' => $responseData]);
|
|
|
+
|
|
|
+ if ($responseData['code'] !== 200) {
|
|
|
+ // API返回错误
|
|
|
+ logDB('generate', 'error', 'NanoBanana任务状态查询失败', ['task_id' => $apiTaskId, 'error' => $responseData['msg'] ?? 'API Error']);
|
|
|
+ return [
|
|
|
+ 'status' => 'failed',
|
|
|
+ 'error_message' => $responseData['msg'] ?? 'API Error',
|
|
|
+ 'result_url' => null,
|
|
|
+ 'result_json' => $responseData
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析API响应
|
|
|
+ $data = $responseData['data'] ?? [];
|
|
|
+ $taskStatus = $data['status'] ?? 0;
|
|
|
+
|
|
|
+ $returnData = [
|
|
|
+ 'status' => 'processing',
|
|
|
+ 'error_message' => null,
|
|
|
+ 'result_url' => null,
|
|
|
+ 'result_json' => $responseData
|
|
|
+ ];
|
|
|
+
|
|
|
+ // status: 2表示成功
|
|
|
+ if ($taskStatus === 2 && isset($data['result']) && !empty($data['result'])) {
|
|
|
+ $result_urls = [];
|
|
|
+ foreach ($data['result'] as $url) {
|
|
|
+ $pic_name = 'ai_generation_nanobanana_' . time() . '_' . uniqid();
|
|
|
+ $pic_ext = getImgExtFromUrl($url);
|
|
|
+ $pic_name = $pic_name . $pic_ext;
|
|
|
+
|
|
|
+ if ($pic_ext === '.png') {
|
|
|
+ $comporessed_data = compressRemoteImageUrlToSize($url);
|
|
|
+ $url = uploadStreamByTos('image', $comporessed_data, $pic_name);
|
|
|
+ } else {
|
|
|
+ // 将图片另存到tos
|
|
|
+ $url = uploadStreamByTos('image', file_get_contents($url), $pic_name);
|
|
|
+ }
|
|
|
+
|
|
|
+ $result_urls[] = $url;
|
|
|
+ }
|
|
|
+ $returnData['result_url'] = $result_urls;
|
|
|
+ $returnData['status'] = 'success';
|
|
|
+ } elseif ($taskStatus === 3) {
|
|
|
+ // status: 3表示失败
|
|
|
+ $returnData['status'] = 'failed';
|
|
|
+ $returnData['error_message'] = $data['message'] ?? '任务执行失败';
|
|
|
+ }
|
|
|
+ // status: 0或1表示处理中,保持processing状态
|
|
|
+
|
|
|
+ return $returnData;
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ return [
|
|
|
+ 'status' => 'failed',
|
|
|
+ 'error_message' => $e->getMessage(),
|
|
|
+ 'result_url' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算最大公约数(用于简化比例)
|
|
|
+ *
|
|
|
+ * @param int $a
|
|
|
+ * @param int $b
|
|
|
+ * @return int
|
|
|
+ */
|
|
|
+ private function gcd(int $a, int $b): int
|
|
|
+ {
|
|
|
+ while ($b != 0) {
|
|
|
+ $temp = $b;
|
|
|
+ $b = $a % $b;
|
|
|
+ $a = $temp;
|
|
|
+ }
|
|
|
+ return $a;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 更新所有待处理任务的状态
|
|
|
*
|
|
|
* @return void
|
|
|
@@ -719,9 +947,9 @@ class AIImageGenerationService
|
|
|
\App\Jobs\ProcessGptImage2GenerationJob::dispatch($task->id)->onConnection('redis');
|
|
|
}
|
|
|
|
|
|
- // 3. 获取所有处理中的任务(仅即梦AI需要查询状态)
|
|
|
+ // 3. 获取所有处理中的任务(即梦AI和NanoBanana系列需要查询状态)
|
|
|
$processingTasks = MpGeneratePicTask::where('status', MpGeneratePicTask::STATUS_PROCESSING)
|
|
|
- ->where('model', self::MODEL_JIMENG_4)
|
|
|
+ ->whereIn('model', array_merge([self::MODEL_JIMENG_4], BaseConst::NANO_BANANA_MODELS))
|
|
|
->get();
|
|
|
|
|
|
foreach ($processingTasks as $task) {
|