Index.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. <?php
  2. namespace app\api\controller;
  3. use app\common\controller\Api;
  4. use app\service\AIGatewayService;
  5. use think\Db;
  6. use think\Exception;
  7. /**
  8. * 本地测试接口
  9. */
  10. class Index extends Api
  11. {
  12. protected $noNeedLogin = ['*'];
  13. protected $noNeedRight = ['*'];
  14. /**
  15. * 首页
  16. */
  17. public function index()
  18. {
  19. $this->success('请求成功');
  20. }
  21. // 向量引擎配置
  22. private $baseUrl = "https://api.vectorengine.ai/v1";
  23. private $apiKey = "sk-P877pnXMk2erRS2an7qEa3Kdb3rIb7JVAWZ39lhA8HeN71gZ"; // 从控制台获取
  24. private $timeout = 120; // 超时时间(秒),视频生成需要更长
  25. /**
  26. * 文生图接口
  27. * POST /api/index/textToImage
  28. * 参数: prompt (string) 提示词
  29. */
  30. public function textToImage()
  31. {
  32. $params = $this->request->param();
  33. if (empty($params)) {
  34. $this->error('提示词不能为空');
  35. }
  36. $data = [
  37. "model" => "gemini-3-pro-image-preview", // 可替换为 dall-e-3 等
  38. "prompt" => $params['prompt'],
  39. "n" => 1,
  40. "size" => "1024x1024"
  41. ];
  42. $result = $this->requestVectorEngine("/images/generations", $data);
  43. if ($result['code'] === 0) {
  44. $this->success('生成成功', $result['data']);
  45. } else {
  46. $this->error($result['msg'], $result['data']);
  47. }
  48. }
  49. /**
  50. * 图生文接口
  51. * POST /api/index/imageToText
  52. * 参数: image_url (string) 公网图片URL, prompt (string) 提问指令
  53. */
  54. public function imageToText()
  55. {
  56. $imageUrl = $this->request->post('image_url');
  57. $prompt = $this->request->post('prompt', '描述这张图片的内容');
  58. if (empty($imageUrl)) {
  59. $this->error('图片URL不能为空');
  60. }
  61. $data = [
  62. "model" => "gpt-4-vision-preview",
  63. "messages" => [
  64. [
  65. "role" => "user",
  66. "content" => [
  67. ["type" => "text", "text" => $prompt],
  68. ["type" => "image_url", "image_url" => ["url" => $imageUrl]]
  69. ]
  70. ]
  71. ],
  72. "max_tokens" => 1000
  73. ];
  74. $result = $this->requestVectorEngine("/chat/completions", $data);
  75. if ($result['code'] === 0) {
  76. $this->success('识别成功', $result['data']);
  77. } else {
  78. $this->error($result['msg'], $result['data']);
  79. }
  80. }
  81. /**
  82. * 文生视频接口
  83. * POST /api/index/textToVideo
  84. * 参数: prompt (string) 提示词, duration (int) 时长(秒), resolution (string) 分辨率
  85. */
  86. public function textToVideo()
  87. {
  88. $prompt = $this->request->post('prompt');
  89. $duration = $this->request->post('duration', 5);
  90. $resolution = $this->request->post('resolution', '720p');
  91. if (empty($prompt)) {
  92. $this->error('提示词不能为空');
  93. }
  94. $data = [
  95. "model" => "kling-1.6", // 可替换为 seedance-2.0 等
  96. "prompt" => $prompt,
  97. "duration" => (int)$duration,
  98. "resolution" => $resolution
  99. ];
  100. $result = $this->requestVectorEngine("/videos/generations", $data);
  101. if ($result['code'] === 0) {
  102. $this->success('生成成功', $result['data']);
  103. } else {
  104. $this->error($result['msg'], $result['data']);
  105. }
  106. }
  107. /**
  108. * 封装向量引擎通用CURL请求
  109. */
  110. private function requestVectorEngine($endpoint, $data)
  111. {
  112. $url = $this->baseUrl . $endpoint;
  113. $ch = curl_init();
  114. curl_setopt_array($ch, [
  115. CURLOPT_URL => $url,
  116. CURLOPT_RETURNTRANSFER => true,
  117. CURLOPT_POST => true,
  118. CURLOPT_POSTFIELDS => json_encode($data),
  119. CURLOPT_HTTPHEADER => [
  120. "Content-Type: application/json",
  121. "Authorization: Bearer " . $this->apiKey
  122. ],
  123. CURLOPT_SSL_VERIFYPEER => false, // 生产环境建议开启
  124. CURLOPT_TIMEOUT => $this->timeout
  125. ]);
  126. $response = curl_exec($ch);
  127. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  128. $error = curl_error($ch);
  129. curl_close($ch);
  130. if ($error) {
  131. return ['code' => -1, 'msg' => '请求失败: ' . $error, 'data' => null];
  132. }
  133. if ($httpCode !== 200) {
  134. return ['code' => $httpCode, 'msg' => 'API返回异常', 'data' => json_decode($response, true)];
  135. }
  136. return ['code' => 0, 'msg' => '成功', 'data' => json_decode($response, true)];
  137. }
  138. public function GetTxtToImg(){
  139. $params = $this->request->param();
  140. $prompt = $params['prompt'];//提示词
  141. $model = $params['model'];//模型
  142. $size = $params['size'];//尺寸
  143. // 调用AI生成图片
  144. $aiGateway = new AIGatewayService();
  145. $res = $aiGateway->callDalleApi($prompt, $model, $size);
  146. // 提取base64图片数据
  147. $imageData = '';
  148. $imageType = 'png'; // 默认图片类型
  149. if (isset($res['candidates'][0]['content']['parts'][0]['text'])) {
  150. $text_content = $res['candidates'][0]['content']['parts'][0]['text'];
  151. // 匹配base64图片数据和类型
  152. if (preg_match('/data:image\/([a-zA-Z0-9]+);base64,([^\s]+)/', $text_content, $matches)) {
  153. $imageType = strtolower($matches[1]);
  154. $base64Data = $matches[2];
  155. // 解码base64数据
  156. $imageData = base64_decode($base64Data);
  157. }
  158. }
  159. if (!$imageData) {
  160. return json(['code' => 1, 'msg' => '图片生成失败,未找到有效图片数据']);
  161. }
  162. // 创建保存目录(public/uploads/log/YYYY-MM/)
  163. $yearMonth = date('Ym');
  164. $saveDir = ROOT_PATH . 'public/uploads/ceshi/' . $yearMonth . '/';
  165. if (!is_dir($saveDir)) {
  166. mkdir($saveDir, 0755, true);
  167. }
  168. // 生成唯一文件名
  169. $fileName = uniqid() . '.' . $imageType;
  170. $filePath = $saveDir . $fileName;
  171. // 保存图片到本地文件
  172. if (file_put_contents($filePath, $imageData) === false) {
  173. return json(['code' => 1, 'msg' => '图片保存失败']);
  174. }
  175. // 生成前端可访问的URL
  176. $imageUrl = '/uploads/ceshi/' . $yearMonth . '/' . $fileName;
  177. // 返回标准JSON响应
  178. return json([
  179. 'code' => 0,
  180. 'msg' => '图片生成成功',
  181. 'data' => [
  182. 'url' => $imageUrl
  183. ]
  184. ]);
  185. }
  186. /**
  187. * 学生端文生视频接口 - 用于生成视频
  188. */
  189. public function GetTxtToVideo(){
  190. $aiGateway = new AIGatewayService();
  191. $apiUrl = $aiGateway->config['videos']['api_url'];
  192. $apiKey = $aiGateway->config['videos']['api_key'];
  193. // 获取并验证参数
  194. $params = $this->request->param();
  195. // echo "<pre>";
  196. // print_r($params);
  197. // echo "<pre>";die;
  198. $postData = [
  199. 'prompt' => $params['prompt'],
  200. 'model' => $params['model'],
  201. 'seconds' => $params['duration'],
  202. 'size' => $params['size'],
  203. ];
  204. // 初始化CURL
  205. $ch = curl_init();
  206. curl_setopt($ch, CURLOPT_URL, $apiUrl);
  207. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  208. curl_setopt($ch, CURLOPT_POST, true);
  209. curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
  210. curl_setopt($ch, CURLOPT_HTTPHEADER, [
  211. 'Authorization: Bearer ' . $apiKey
  212. ]);
  213. curl_setopt($ch, CURLOPT_TIMEOUT, 300);
  214. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  215. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
  216. curl_setopt($ch, CURLOPT_HEADER, true); // 获取响应头
  217. curl_setopt($ch, CURLOPT_VERBOSE, true); // 启用详细输出以进行调试
  218. // 创建临时文件来捕获详细的cURL输出
  219. $verbose = fopen('php://temp', 'w+');
  220. curl_setopt($ch, CURLOPT_STDERR, $verbose);
  221. // 执行请求
  222. $response = curl_exec($ch);
  223. //HTTP状态码
  224. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  225. // 获取详细的cURL调试信息
  226. rewind($verbose);
  227. //CURL调试信息
  228. $verboseLog = stream_get_contents($verbose);
  229. fclose($verbose);
  230. // 分离头部和主体
  231. $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
  232. //响应头部
  233. $header = substr($response, 0, $header_size);
  234. //响应主体
  235. $body = substr($response, $header_size);
  236. // 检查CURL错误
  237. $curlError = curl_error($ch);
  238. curl_close($ch);
  239. $responseData = json_decode($body, true);
  240. // 检查API是否返回了错误信息
  241. if (isset($responseData['error'])) {
  242. $errorMessage = isset($responseData['error']['message']) ? $responseData['error']['message'] : 'API请求失败';
  243. return json([
  244. 'code' => 1,
  245. 'msg' => '视频生成请求失败',
  246. 'data' => [
  247. 'error_type' => isset($responseData['error']['type']) ? $responseData['error']['type'] : 'unknown',
  248. 'error_code' => isset($responseData['error']['code']) ? $responseData['error']['code'] : 'unknown',
  249. 'error_message' => $errorMessage
  250. ]
  251. ]);
  252. }
  253. // 检查是否有自定义错误格式
  254. if (isset($responseData['code']) && $responseData['code'] === 'fail_to_fetch_task' && isset($responseData['message'])) {
  255. return json([
  256. 'code' => 1,
  257. 'msg' => '视频生成请求失败',
  258. 'data' => [
  259. 'error_code' => $responseData['code'],
  260. 'error_message' => $responseData['message']
  261. ]
  262. ]);
  263. }
  264. // 检查是否存在id字段
  265. if (!isset($responseData['id'])) {
  266. return json([
  267. 'code' => 1,
  268. 'msg' => '无法获取视频ID',
  269. 'data' => [
  270. 'response_data' => $responseData,
  271. 'http_code' => $httpCode
  272. ]
  273. ]);
  274. }
  275. // 1. 先检查视频状态
  276. $statusUrl = 'https://chatapi.onechats.ai/v1/videos/' . $responseData['id'];
  277. $statusData = $this->fetchVideoStatus($statusUrl, $apiKey);
  278. // 检查视频状态
  279. if ($statusData['status'] !== 'completed') {
  280. return json([
  281. 'code' => 202,
  282. 'msg' => '视频尚未生成完成',
  283. 'data' => [
  284. 'video_id' => $responseData['id'],
  285. 'status' => $statusData['status'],
  286. 'progress' => $statusData['progress'],
  287. 'created_at' => $statusData['created_at'],
  288. 'message' => '请稍后再试,视频仍在' . ($statusData['status'] === 'queued' ? '排队中' : '处理中')
  289. ]
  290. ]);
  291. }
  292. // 2. 视频生成完成,准备下载
  293. $apiUrl = 'https://chatapi.onechats.ai/v1/videos/' . $responseData['id'] . '/content';
  294. // 获取可选的variant参数
  295. $variant = $this->request->get('variant', '');
  296. if (!empty($variant)) {
  297. $apiUrl .= '?variant=' . urlencode($variant);
  298. }
  299. // 创建保存目录
  300. $saveDir = ROOT_PATH . 'public' . DS . 'uploads' . DS . 'videos' . DS . date('Ymd');
  301. if (!is_dir($saveDir)) {
  302. mkdir($saveDir, 0755, true);
  303. }
  304. // 生成唯一文件名
  305. $filename = $responseData['id'] . '.mp4';
  306. $localPath = DS . 'uploads' . DS . 'videos' . DS . date('Ymd') . DS . $filename;
  307. $fullPath = $saveDir . DS . $filename;
  308. // 3. 下载视频
  309. $videoData = $this->downloadVideo($apiUrl, $apiKey);
  310. // 4. 保存视频文件
  311. if (file_put_contents($fullPath, $videoData) === false) {
  312. throw new Exception('视频保存失败');
  313. }
  314. // 确保路径使用正斜杠,并只保存相对路径部分
  315. $localPath = str_replace('\\', '/', $localPath);
  316. // 移除开头的斜杠,确保路径格式为uploads/videos/...
  317. $savePath = ltrim($localPath, '/');
  318. // 返回成功响应
  319. return json([
  320. 'code' => 0,
  321. 'msg' => '视频下载成功',
  322. 'data' => [
  323. 'video_id' => $responseData['id'],
  324. 'web_url' => $savePath
  325. ]
  326. ]);
  327. }
  328. /**
  329. * 学生端视频状态查询接口 - 用于通过video_id查询视频状态
  330. */
  331. public function Getvideo_id(){
  332. // 获取并验证参数
  333. $params = $this->request->param();
  334. $videoId = $params['video_id'] ?? '';
  335. // 验证video_id参数
  336. if (empty($videoId)) {
  337. return json([
  338. 'code' => 1,
  339. 'msg' => '缺少必要参数:video_id',
  340. 'data' => []
  341. ]);
  342. }
  343. $aiGateway = new AIGatewayService();
  344. // $apiUrl = $aiGateway->config['videos']['api_url'];
  345. $apiKey = $aiGateway->config['videos']['api_key'];
  346. $apiUrl = 'https://chatapi.onechats.ai/v1/videos/' . $videoId;
  347. // 先检查本地是否已经有该视频
  348. $localVideoPath = ROOT_PATH . 'public' . DS . 'uploads' . DS . 'videos' . DS . date('Ymd') . DS . $videoId . '.mp4';
  349. if (file_exists($localVideoPath)) {
  350. // 视频已存在本地,直接返回
  351. $webPath = '/uploads/videos/' . date('Ymd') . '/' . $videoId . '.mp4';
  352. return json([
  353. 'code' => 0,
  354. 'msg' => '视频下载成功',
  355. 'data' => [
  356. 'video_id' => $videoId,
  357. 'web_url' => substr($webPath, 1) // 移除开头的斜杠
  358. ]
  359. ]);
  360. }
  361. // 检查视频状态
  362. $statusData = $this->fetchVideoStatus($apiUrl, $apiKey);
  363. // 检查视频状态
  364. if ($statusData['status'] !== 'completed') {
  365. return json([
  366. 'code' => 202,
  367. 'msg' => '视频尚未生成完成',
  368. 'data' => [
  369. 'video_id' => $videoId,
  370. 'status' => $statusData['status'],
  371. 'progress' => isset($statusData['progress']) ? $statusData['progress'] : 0,
  372. 'created_at' => $statusData['created_at'],
  373. 'message' => '请稍后再试,视频仍在' . ($statusData['status'] === 'queued' ? '排队中' : '处理中')
  374. ]
  375. ]);
  376. }
  377. // 视频生成完成,下载视频
  378. $downloadUrl = 'https://chatapi.onechats.ai/v1/videos/' . $videoId . '/content';
  379. // 获取可选的variant参数
  380. $variant = $this->request->get('variant', '');
  381. if (!empty($variant)) {
  382. $downloadUrl .= '?variant=' . urlencode($variant);
  383. }
  384. // 创建保存目录
  385. $saveDir = ROOT_PATH . 'public' . DS . 'uploads' . DS . 'videos' . DS . date('Ymd');
  386. if (!is_dir($saveDir)) {
  387. mkdir($saveDir, 0755, true);
  388. }
  389. // 生成唯一文件名
  390. $filename = $videoId . '.mp4';
  391. $localPath = DS . 'uploads' . DS . 'videos' . DS . date('Ymd') . DS . $filename;
  392. $fullPath = $saveDir . DS . $filename;
  393. // 下载视频
  394. $videoData = $this->downloadVideo($downloadUrl, $apiKey);
  395. // 保存视频文件
  396. if (file_put_contents($fullPath, $videoData) === false) {
  397. throw new Exception('视频保存失败');
  398. }
  399. // 确保路径使用正斜杠,并只保存相对路径部分
  400. $localPath = str_replace(DIRECTORY_SEPARATOR, '/', $localPath);
  401. // 移除开头的斜杠,确保路径格式为uploads/videos/...
  402. $savePath = ltrim($localPath, '/');
  403. // 返回成功响应
  404. return json([
  405. 'code' => 0,
  406. 'msg' => '视频下载成功',
  407. 'data' => [
  408. 'video_id' => $videoId,
  409. 'web_url' => $savePath
  410. ]
  411. ]);
  412. }
  413. /**
  414. * 九个分镜头生成流程
  415. * 模型:gemini-3-pro-image-preview
  416. * 说明:
  417. * 第一步:(提示词 + 原始图片 = 九个分镜头图片) 或 (提示词 = 九个分镜头图片)
  418. * 第二步:使用九个分镜头图进行裁剪单图生成连贯视频
  419. * 第三步:在通过分镜头视频拼接成一个完整的视频(列如每个分镜头视频为8秒,九个为72秒形成完整视频)
  420. */
  421. public function Get_txttonineimg()
  422. {
  423. // 发起接口请求
  424. // $apiUrl = 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:generateContent';
  425. $apiUrl = 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:streamGenerateContent';
  426. $apiKey = '';
  427. // $params = $this->request->param();
  428. $prompt = '生成一个苹果(九个分镜头图片)';
  429. $requestData = [
  430. "contents" => [
  431. [
  432. "role" => "user",
  433. "parts" => [
  434. ["text" => $prompt]
  435. ]
  436. ]
  437. ],
  438. "generationConfig" => [
  439. "responseModalities" => ["TEXT", "IMAGE"],
  440. "imageConfig" => [
  441. "aspectRatio" => "1:1"
  442. ]
  443. ]
  444. ];
  445. $ch = curl_init();
  446. curl_setopt($ch, CURLOPT_URL, $apiUrl);
  447. curl_setopt($ch, CURLOPT_POST, true);
  448. curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData, JSON_UNESCAPED_UNICODE));
  449. curl_setopt($ch, CURLOPT_HTTPHEADER, [
  450. 'Content-Type: application/json',
  451. 'Authorization: Bearer ' . $apiKey
  452. ]);
  453. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  454. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 开发环境临时关闭SSL验证
  455. curl_setopt($ch, CURLOPT_TIMEOUT, 60); // 生成图片超时时间(建议60秒)
  456. $response = curl_exec($ch);
  457. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  458. $error = curl_error($ch);
  459. curl_close($ch);
  460. $res = json_decode($response, true);
  461. // 构建URL路径(使用正斜杠)
  462. $url_path = '/uploads/txtnewimg/';
  463. // 构建物理路径(使用正斜杠确保统一格式)
  464. $save_path = ROOT_PATH . 'public' . '/' . 'uploads' . '/' . 'txtnewimg' . '/';
  465. // 移除ROOT_PATH中可能存在的反斜杠,确保统一使用正斜杠
  466. $save_path = str_replace('\\', '/', $save_path);
  467. // 自动创建文件夹(如果不存在)
  468. if (!is_dir($save_path)) {
  469. mkdir($save_path, 0755, true);
  470. }
  471. // 提取base64图片数据
  472. $text_content = $res['candidates'][0]['content']['parts'][0]['inlineData']['data'];
  473. $str = 'data:image/jpeg;base64,';
  474. $text_content = $str. $text_content;
  475. // 匹配base64图片数据
  476. preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $text_content, $matches);
  477. if (empty($matches)) {
  478. return '未找到图片数据';
  479. }
  480. $image_type = $matches[1];
  481. $base64_data = $matches[2];
  482. // 解码base64数据
  483. $image_data = base64_decode($base64_data);
  484. if ($image_data === false) {
  485. return '图片解码失败';
  486. }
  487. // 生成唯一文件名(包含扩展名)
  488. $file_name = uniqid() . '.' . $image_type;
  489. $full_file_path = $save_path . $file_name;
  490. // 保存图片到文件系统
  491. if (!file_put_contents($full_file_path, $image_data)) {
  492. return '图片保存失败';
  493. }
  494. // 生成数据库存储路径(使用正斜杠格式)
  495. $db_img_path = $url_path . $file_name;
  496. return $db_img_path;
  497. }
  498. }