DeepSeekService.php 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795
  1. <?php
  2. namespace App\Services\DeepSeek;
  3. use App\Consts\ErrorConst;
  4. use App\Facade\Site;
  5. use App\Libs\Utils;
  6. use GuzzleHttp\Client;
  7. use DateTime;
  8. use DateTimeZone;
  9. use Illuminate\Support\Facades\DB;
  10. use Illuminate\Support\Facades\Log;
  11. use Illuminate\Support\Facades\Redis;
  12. use OSS\Core\OssException;
  13. use OSS\OssClient;
  14. class DeepSeekService
  15. {
  16. private $url;
  17. private $api_key;
  18. private $client;
  19. private $headers;
  20. public function __construct() {
  21. $this->url = 'https://api.deepseek.com/chat/completions';
  22. $this->api_key = env('DEEPSEEK_API_KEY');
  23. $this->headers = [
  24. 'Authorization' => 'Bearer '.$this->api_key,
  25. 'Content-Type' => 'application/json; charset=UTF-8'
  26. ];
  27. }
  28. // 与推理模型对话
  29. public function chatWithReasoner($data) {
  30. $content = getProp($data, 'content');
  31. $model = getProp($data, 'model', 'r1');
  32. $model = $model == 'r1' ? 'deepseek-reasoner' : 'deepseek-chat';
  33. // 获取可选情感(根据音色可支持情感选)
  34. $timbre_emotion = DB::table('mp_timbres')->where('is_enabled', 1)->pluck('emotion')->toArray();
  35. $emotion_list = [];
  36. foreach ($timbre_emotion as $emotion) {
  37. $tmp = explode(',', $emotion);
  38. $emotion_list = array_merge($emotion_list, $tmp);
  39. }
  40. $emotion_list = array_unique($emotion_list);
  41. $emotion_str = implode('、', $emotion_list);
  42. // // 获取可选情感
  43. // $emotion_list = DB::table('mp_emotion_list')->where('is_enabled', 1)->select('emotion_name')->orderBy('id')->get()->pluck('emotion_name')->toArray();
  44. // $emotion_str = implode('、', $emotion_list);
  45. // 是否启用情感
  46. $enable_emotion = getProp($data, 'enable_emotion', 0);
  47. if ($enable_emotion) {
  48. $sys_content = "下面有一段小说文本,请帮我将文本中的每句话按从上到下的顺序拆分成角色不同的剧本文稿(不得更改上下文顺序和内容),文稿形式严格按照“角色名(男、女、中性):台词{情感}”输出,需要注意以下几点要求:
  49. 1.角色名后不要加入任何其他词语,只能加性别,在男、女或中性中选
  50. 2.非对话部分请全部用旁白角色代替,旁白的情感统一选择中性
  51. 3.情感必须在【{$emotion_str}】中选一个,不得使用其他词语";
  52. }else {
  53. $sys_content = "下面有一段小说文本,请帮我将文本中的每句话按从上到下的顺序拆分成角色不同的剧本文稿(不得更改上下文顺序和内容),文稿形式严格按照“角色名(男、女、中性):台词”输出,需要注意以下几点要求:
  54. 1.角色名后不要加入任何其他词语,只能加性别,在男、女或中性中选
  55. 2.非对话部分请全部用旁白角色代替,旁白的情感统一选择中性";
  56. }
  57. $messages = [
  58. [
  59. 'role' => 'system',
  60. 'content' => $sys_content
  61. ],
  62. [
  63. 'role' => 'user',
  64. 'content' => $content
  65. ]
  66. ];
  67. $post_data = [
  68. 'model' => $model, // R1模型: deepseek-reasoner V3模型: deepseek-chat
  69. 'messages' => $messages,
  70. 'max_tokens' => 8192,
  71. 'temperature' => 1, // 采样温度,介于 0 和 2 之间。更高的值,如 0.8,会使输出更随机,而更低的值,如 0.2,会使其更加集中和确定。 我们通常建议可以更改这个值或者更改 top_p,但不建议同时对两者进行修改。
  72. // 'top_p' => 1, // 作为调节采样温度的替代方案(<=1),模型会考虑前 top_p 概率的 token 的结果。所以 0.1 就意味着只有包括在最高 10% 概率中的 token 会被考虑。 我们通常建议修改这个值或者更改 temperature,但不建议同时对两者进行修改。
  73. 'frequency_penalty' => 0, // 介于 -2.0 和 2.0 之间的数字。如果该值为正,那么新 token 会根据其在已有文本中的出现频率受到相应的惩罚,降低模型重复相同内容的可能性。
  74. 'presence_penalty' => 0, // 介于 -2.0 和 2.0 之间的数字。如果该值为正,那么新 token 会根据其是否已在已有文本中出现受到相应的惩罚,从而增加模型谈论新主题的可能性。
  75. 'response_format' => [
  76. 'type' => 'text' // 默认值text,回答的结果输出文字(非接口返回值是text,接口返回值还是json字串),还可选:json_object,输出json格式
  77. ],
  78. 'stream' => false // 是否流式输出,如果设置为 True,将会以 SSE(server-sent events)的形式以流式发送消息增量。消息流以 data: [DONE] 结尾。
  79. ];
  80. $client = new Client(['timeout' => 1200, 'verify' => false]);
  81. $result = $client->post($this->url, ['json' => $post_data, 'headers' => $this->headers]);
  82. $response = $result->getBody()->getContents();
  83. $response_arr = json_decode($response, true);
  84. $update_data = [];
  85. $content = '';
  86. if (isset($response_arr['choices']) && count($response_arr['choices']) > 0) {
  87. $content = isset($response_arr['choices'][0]['message']['content']) ? $response_arr['choices'][0]['message']['content'] : '';
  88. $update_data = [
  89. 'role' => 'assistant',
  90. 'content' => $response_arr['choices'][0]['message']['content'],
  91. 'usage' => isset($response_arr['usage']) ? $response_arr['usage'] : []
  92. ];
  93. }
  94. // 处理获取到的剧本数据
  95. $script_content = handleScriptWords($content, $enable_emotion);
  96. $result = [
  97. 'origin_content' => $content,
  98. 'roles' => getProp($script_content, 'roles'),
  99. 'words' => getProp($script_content, 'words'),
  100. ];
  101. return $result;
  102. }
  103. public function resetParagraphAudio($data) {
  104. $bid = getProp($data, 'bid');
  105. $cid = getProp($data, 'cid');
  106. $version_id = getProp($data, 'version_id');
  107. if (!DB::table('mp_chapter_paragraph_audios')->where('bid', $bid)->where('cid', $cid)->where('version_id', $version_id)->value('id')) return true;
  108. return DB::table('mp_chapter_paragraph_audios')->where('bid', $bid)->where('cid', $cid)->where('version_id', $version_id)->update([
  109. 'generate_status' => '待制作',
  110. 'updated_at' => date('Y-m-d H:i:s')
  111. ]);
  112. }
  113. public function saveParagraphAudio($data) {
  114. $bid = getProp($data, 'bid');
  115. $cid = getProp($data, 'cid');
  116. $version_id = getProp($data, 'version_id');
  117. $sequence = getProp($data, 'sequence');
  118. $id = DB::table('mp_chapter_paragraph_audios')->where('bid', $bid)->where('cid', $cid)->where('version_id', $version_id)->where('sequence', $sequence)->value('id');
  119. // 获取所有情感
  120. $emotion_list = DB::table('mp_emotion_list')->where('is_enabled', 1)->pluck('emotion_name', 'emotion_code')->toArray();
  121. $emotion_list = array_flip($emotion_list);
  122. // 获取音色支持情感
  123. $timbre_emotion = DB::table('mp_timbres')->where('timbre_type', getProp($data, 'voice_type'))->value('emotion');
  124. $timbre_emotion = explode(',', $timbre_emotion);
  125. $emotion = getProp($data, 'emotion');
  126. if (!in_array($emotion, $timbre_emotion)) $emotion = '中性';
  127. $emotion_type = isset($emotion_list[$emotion]) ? $emotion_list[$emotion] : 'neutral';
  128. $list = [
  129. 'bid' => $bid,
  130. 'cid' => $cid,
  131. 'version_id' => $version_id,
  132. 'sequence' => $sequence,
  133. 'role' => getProp($data, 'role'),
  134. 'gender' => getProp($data, 'gender'),
  135. 'text' => trim(getProp($data, 'text')),
  136. 'emotion' => $emotion,
  137. 'emotion_type' => $emotion_type,
  138. 'voice_type' => getProp($data, 'voice_type'),
  139. 'voice_name' => getProp($data, 'voice_name'),
  140. 'speed_ratio' => getProp($data, 'speed_ratio', 0),
  141. 'loudness_ratio'=> getProp($data, 'loudness_ratio', 0),
  142. 'emotion_scale' => getProp($data, 'emotion_scale', 4),
  143. 'pitch' => getProp($data, 'pitch', 0),
  144. // 'paragraph_audio_url' => '',
  145. 'generate_status' => '制作中',
  146. 'error_msg' => '',
  147. 'updated_at' => date('Y-m-d H:i:s')
  148. ];
  149. $continue = false;
  150. // 判断是否含有中文、数字或英文,如果没有则跳过合成
  151. if (!preg_match('/[\x{4e00}-\x{9fa5}]+/u', $list['text']) && !preg_match('/[0-9]+/', $list['text']) && !preg_match('/[a-zA-Z]+/', $list['text'])) {
  152. $list['generate_status'] = '制作成功';
  153. $list['paragraph_audio_url'] = 'https://zw-audiobook.tos-cn-beijing.volces.com/effects/ellipses_2s.mp3';
  154. $list['subtitle_info'] = '{"duration": 2.0, "words": []}';
  155. $continue = true;
  156. }
  157. // if (getProp($data, 'paragraph_audio_url')) $list['paragraph_audio_url'] = getProp($data, 'paragraph_audio_url');
  158. if ($id) {
  159. $boolen = DB::table('mp_chapter_paragraph_audios')->where('id', $id)->update($list);
  160. }else {
  161. $list['created_at'] = date('Y-m-d H:i:s');
  162. $id = DB::table('mp_chapter_paragraph_audios')->insertGetId($list);
  163. $boolen = $id ? true : false;
  164. }
  165. // 如果更新成功则加入查询队列
  166. if ($boolen) {
  167. $redis_key = "select-{$bid}-{$version_id}-{$cid}";
  168. Redis::sadd($redis_key, $id);
  169. }
  170. if ($boolen && !$continue) {
  171. $boolen = false;
  172. $client = new Client(['timeout' => 300, 'verify' => false]);
  173. // 根据ID通过API通知合成音频
  174. // $result = $client->get("http://47.240.171.155:5000/api/previewTask?taskId={$id}");
  175. $result = $client->get("http://122.9.129.83:5000/api/previewTask?taskId={$id}");
  176. $response = $result->getBody()->getContents();
  177. $response_arr = json_decode($response, true);
  178. if (!isset($response_arr['code']) || (int)$response_arr['code'] !== 0) {
  179. $error_msg = isset($response_arr['msg']) ? $response_arr['msg'] : '未知错误';
  180. Log::info('通知火山生成段落音频失败: '.$error_msg);
  181. Utils::throwError('20003:通知火山生成段落音频失败');
  182. }
  183. $boolen = true;
  184. }
  185. return $boolen;
  186. }
  187. public function insertAudioEffect($data) {
  188. $audio_id = getProp($data, 'audio_id');
  189. $chapter_audio = DB::table('mp_chapter_audios')->where('id', $audio_id)->first();
  190. $bid = getProp($chapter_audio, 'bid');
  191. $version_id = getProp($chapter_audio, 'version_id');
  192. $cid = getProp($chapter_audio, 'cid');
  193. if (getProp($chapter_audio, 'generate_status') != '制作成功') Utils::throwError('20003:请先完成有声制作');
  194. $audio_effect_json = getProp($data, 'audio_effect_json');
  195. if (!is_string($audio_effect_json)) $audio_effect_json = json_encode($audio_effect_json, 256);
  196. try {
  197. DB::beginTransaction();
  198. $count = DB::table('mp_audio_tasks')->where('audio_id', $audio_id)->count('id');
  199. if (!$count) {
  200. $task_name = getProp($chapter_audio, 'book_name').' '.getProp($chapter_audio, 'chapter_name').'【'.getProp($chapter_audio, 'version_name').'】';
  201. }else {
  202. $task_name = getProp($chapter_audio, 'book_name').' '.getProp($chapter_audio, 'chapter_name').'【'.getProp($chapter_audio, 'version_name').'】('.($count+1).')';
  203. }
  204. $boolen1 = DB::table('mp_chapter_audios')->where('id', $audio_id)->update([
  205. 'audio_effect_status' => '添加中',
  206. 'audio_effect_json' => $audio_effect_json,
  207. 'updated_at' => date('Y-m-d H:i:s')
  208. ]);
  209. if (!$boolen1) {
  210. DB::rollBack();
  211. Utils::throwError('20003:更新生成数据失败');
  212. }
  213. $id = DB::table('mp_audio_tasks')->insertGetId([
  214. 'audio_id' => $audio_id,
  215. 'generate_status' => '执行中',
  216. 'generate_json' => json_encode([], 256),
  217. 'bid' => $bid,
  218. 'book_name' => getProp($chapter_audio, 'book_name'),
  219. 'version_id' => $version_id,
  220. 'version_name' => getProp($chapter_audio, 'version_name'),
  221. 'cid' => $cid,
  222. 'chapter_name' => getProp($chapter_audio, 'chapter_name'),
  223. 'task_name' => $task_name,
  224. 'created_at' => date('Y-m-d H:i:s'),
  225. 'updated_at' => date('Y-m-d H:i:s')
  226. ]);
  227. if (!$id) {
  228. DB::rollBack();
  229. Utils::throwError('20003:创建任务失败');
  230. }
  231. }catch (\Exception $e) {
  232. DB::rollBack();
  233. Utils::throwError('20003:'.$e->getMessage());
  234. }
  235. DB::commit();
  236. $client = new Client(['timeout' => 300, 'verify' => false]);
  237. // 根据ID通过API通知合成音频
  238. // $result = $client->get("http://47.240.171.155:5000/api/postTask?taskId={$id}");
  239. $result = $client->get("http://122.9.129.83:5000/api/postTask?taskId={$id}");
  240. $response = $result->getBody()->getContents();
  241. $response_arr = json_decode($response, true);
  242. if (!isset($response_arr['code']) || (int)$response_arr['code'] !== 0) {
  243. $error_msg = isset($response_arr['msg']) ? $response_arr['msg'] : '未知错误';
  244. Log::info('通知火山插入音效失败: '.$error_msg);
  245. Utils::throwError('20003:通知火山插入音效失败');
  246. }
  247. return true;
  248. }
  249. public function insertBgm($data) {
  250. $audio_id = getProp($data, 'audio_id');
  251. $chapter_audio = DB::table('mp_chapter_audios')->where('id', $audio_id)->first();
  252. $bid = getProp($chapter_audio, 'bid');
  253. $version_id = getProp($chapter_audio, 'version_id');
  254. $cid = getProp($chapter_audio, 'cid');
  255. if (getProp($chapter_audio, 'generate_status') != '制作成功') Utils::throwError('20003:请先完成有声制作');
  256. $bgm_json = getProp($data, 'bgm_json');
  257. if (!is_string($bgm_json)) $bgm_json = json_encode($bgm_json, 256);
  258. try {
  259. DB::beginTransaction();
  260. $count = DB::table('mp_audio_tasks')->where('audio_id', $audio_id)->count('id');
  261. if (!$count) {
  262. $task_name = getProp($chapter_audio, 'book_name').' '.getProp($chapter_audio, 'chapter_name').'【'.getProp($chapter_audio, 'version_name').'】';
  263. }else {
  264. $task_name = getProp($chapter_audio, 'book_name').' '.getProp($chapter_audio, 'chapter_name').'【'.getProp($chapter_audio, 'version_name').'】('.($count+1).')';
  265. }
  266. $boolen1 = DB::table('mp_chapter_audios')->where('id', $audio_id)->update([
  267. 'bgm_status' => '添加中',
  268. 'bgm_json' => $bgm_json,
  269. 'updated_at' => date('Y-m-d H:i:s')
  270. ]);
  271. if (!$boolen1) {
  272. DB::rollBack();
  273. Utils::throwError('20003:更新生成数据失败');
  274. }
  275. $id = DB::table('mp_audio_tasks')->insertGetId([
  276. 'audio_id' => $audio_id,
  277. 'generate_status' => '执行中',
  278. 'generate_json' => json_encode([], 256),
  279. 'bid' => $bid,
  280. 'book_name' => getProp($chapter_audio, 'book_name'),
  281. 'version_id' => $version_id,
  282. 'version_name' => getProp($chapter_audio, 'version_name'),
  283. 'cid' => $cid,
  284. 'chapter_name' => getProp($chapter_audio, 'chapter_name'),
  285. 'task_name' => $task_name,
  286. 'created_at' => date('Y-m-d H:i:s'),
  287. 'updated_at' => date('Y-m-d H:i:s')
  288. ]);
  289. if (!$id) {
  290. DB::rollBack();
  291. Utils::throwError('20003:创建任务失败');
  292. }
  293. }catch (\Exception $e) {
  294. DB::rollBack();
  295. Utils::throwError('20003:'.$e->getMessage());
  296. }
  297. DB::commit();
  298. $client = new Client(['timeout' => 300, 'verify' => false]);
  299. // 根据ID通过API通知合成音频
  300. // $result = $client->get("http://47.240.171.155:5000/api/postTask?taskId={$id}");
  301. $result = $client->get("http://122.9.129.83:5000/api/postTask?taskId={$id}");
  302. $response = $result->getBody()->getContents();
  303. $response_arr = json_decode($response, true);
  304. if (!isset($response_arr['code']) || (int)$response_arr['code'] !== 0) {
  305. $error_msg = isset($response_arr['msg']) ? $response_arr['msg'] : '未知错误';
  306. Log::info('通知火山插入bgm失败: '.$error_msg);
  307. Utils::throwError('20003:通知火山插入bgm失败');
  308. }
  309. return true;
  310. }
  311. // 新增合成任务
  312. public function addGenerateTask($data) {
  313. $bid = getProp($data, 'bid');
  314. $cid = getProp($data, 'cid');
  315. $version_id = getProp($data, 'version_id');
  316. $generate_json = getProp($data, 'generate_json');
  317. // 获取已生成的音频
  318. $paragraph_audios = DB::table('mp_chapter_paragraph_audios')->where('bid', $bid)->where('cid', $cid)->where('version_id', $version_id)->get();
  319. $paragraph_list = [];
  320. foreach($paragraph_audios as $item) {
  321. $paragraph_list[getProp($item, 'sequence')] = [
  322. 'role' => getProp($item, 'role'),
  323. 'gender' => getProp($item, 'gender'),
  324. 'text' => trim(getProp($item, 'text')),
  325. 'emotion' => getProp($item, 'emotion'),
  326. 'emotion_type' => getProp($item, 'emotion_type'),
  327. 'voice_type' => getProp($item, 'voice_type'),
  328. 'voice_name' => getProp($item, 'voice_name'),
  329. 'speed_ratio' => getProp($item, 'speed_ratio'),
  330. 'loudness_ratio' => getProp($item, 'loudness_ratio'),
  331. 'emotion_scale' => getProp($item, 'emotion_scale'),
  332. 'paragraph_audio_url' => getProp($item, 'paragraph_audio_url'),
  333. ];
  334. }
  335. // 更新角色-音色信息
  336. $existed_role_info = DB::table('mp_book_version')->where('bid', $bid)->where('id', $version_id)->value('role_info');
  337. $existed_role_info = json_decode($existed_role_info, true);
  338. if (!$existed_role_info) $existed_role_info = [];
  339. // 获取情感信息
  340. $emotion_list = DB::table('mp_emotion_list')->where('is_enabled', 1)->pluck('emotion_name', 'emotion_code')->toArray();
  341. $emotion_list = array_flip($emotion_list);
  342. // 获取音色对应情感组
  343. $timbre_emotions = DB::table('mp_timbres')->where('is_enabled', 1)->select('timbre_type', 'emotion')->get();
  344. $timbre_emotion_list = [];
  345. foreach($timbre_emotions as $item) {
  346. $timbre_emotion_list[getProp($item, 'timbre_type')] = explode(',', getProp($item, 'emotion'));
  347. }
  348. // 构造生成音频的json
  349. $words = json_decode($generate_json, true);
  350. // 最终合成前的参数组
  351. $mp_chapter_paragraph_audios = [];
  352. $sequence = 1;
  353. foreach($words as &$word) {
  354. if (!isset($word['text']) || !isset($word['emotion']) || !isset($word['voice_type']) || !isset($word['voice_name']) || !isset($word['speed_ratio']) || !isset($word['loudness_ratio']) || !isset($word['emotion_scale'])) Utils::throwError('20003:参数格式有误');
  355. if (!($word['text']) || !($word['voice_type']) || !($word['voice_name'])) Utils::throwError('20003:参数不得为空');
  356. $role = getProp($word, 'role');
  357. $word['gender'] = (int)$word['gender'];
  358. // 判断音色对应情感是否支持,不支持则调整为中性
  359. $access_emotion = isset($timbre_emotion_list[$word['voice_type']]) ? $timbre_emotion_list[$word['voice_type']] : [];
  360. if (!in_array($word['emotion'], $access_emotion)) $word['emotion'] = '中性';
  361. // 如果有对应情感则赋值,没有则默认为中性(neutral)
  362. if (isset($emotion_list[getProp($word, 'emotion')])) {
  363. $word['emotion_type'] = $emotion_list[getProp($word, 'emotion')];
  364. }else {
  365. $word['emotion'] = '中性';
  366. $word['emotion_type'] = 'neutral';
  367. }
  368. $existed_role_info[$role] = [
  369. 'timbre_type' => $word['voice_type'],
  370. 'timbre_name' => $word['voice_name'],
  371. 'emotion' => $word['emotion'],
  372. 'emotion_type' => $word['emotion_type'],
  373. 'speed_ratio' => $word['speed_ratio'],
  374. 'loudness_ratio'=> $word['loudness_ratio'],
  375. 'emotion_scale' => $word['emotion_scale'],
  376. 'pitch' => $word['pitch']
  377. ];
  378. $word['paragraph_audio_url'] = '';
  379. // 判断生成参数是否相同,相同则直接使用已生成的音频
  380. $paragraph = isset($paragraph_list[$sequence]) ? $paragraph_list[$sequence] : [];
  381. // 如果音频存在并且参数都未改变则使用已生成的音频
  382. if (getProp($paragraph, 'paragraph_audio_url')) {
  383. if (getProp($paragraph, 'role') == getProp($word, 'role') && getProp($paragraph, 'text') == getProp($word, 'text') &&
  384. getProp($paragraph, 'voice_type') == getProp($word, 'voice_type') &&
  385. getProp($paragraph, 'speed_ratio') == getProp($word, 'speed_ratio') && getProp($paragraph, 'loudness_ratio') == getProp($word, 'loudness_ratio') &&
  386. getProp($paragraph, 'emotion_scale') == getProp($word, 'emotion_scale'))
  387. {
  388. $word['paragraph_audio_url'] = getProp($paragraph, 'paragraph_audio_url');
  389. }
  390. }
  391. $tmp = $word;
  392. // 组装章节分句音频数据
  393. // $tmp['sequence'] = getProp($word, 'sequence');
  394. // $tmp['text'] = getProp($word, 'text');
  395. // $tmp['emotion'] = getProp($word, 'emotion');
  396. // $tmp['emotion_type'] = getProp($word, 'emotion_type');
  397. // $tmp['voice_name'] = getProp($word, 'voice_name');
  398. // $tmp['voice_type'] = getProp($word, 'voice_type');
  399. // $tmp['speed_ratio'] = getProp($word, 'speed_ratio');
  400. // $tmp['loudness_ratio'] = getProp($word, 'loudness_ratio');
  401. // $tmp['emotion_scale'] = getProp($word, 'emotion_scale');
  402. $tmp['bid'] = $bid;
  403. $tmp['version_id'] = $version_id;
  404. $tmp['cid'] = $cid;
  405. $tmp['sequence'] = $sequence;
  406. $tmp['created_at'] = date('Y-m-d H:i:s');
  407. $tmp['updated_at'] = date('Y-m-d H:i:s');
  408. if (!getProp($tmp, 'paragraph_audio_url')) Utils::throwError('20003:段落未生成音频,请等待完成后提交');
  409. $mp_chapter_paragraph_audios[] = $tmp;
  410. $sequence++;
  411. }
  412. $generate_json = json_encode($words, 256);
  413. try {
  414. DB::beginTransaction();
  415. $role_info = json_encode($existed_role_info, 256);
  416. $boolen = DB::table('mp_book_version')->where('bid', $bid)->where('id', $version_id)->update(['role_info' => $role_info, 'updated_at' => date('Y-m-d H:i:s')]);
  417. if (!$boolen) {
  418. DB::rollBack();
  419. Utils::throwError('20003:更新角色信息失败');
  420. }
  421. $count = DB::table('mp_audio_tasks')->where('bid', $bid)->where('version_id', $version_id)->where('cid', $cid)->count('id');
  422. $chapter_audio = DB::table('mp_chapter_audios')->where('bid', $bid)->where('version_id', $version_id)->where('cid', $cid)->first();
  423. if (!$count) {
  424. $task_name = getProp($chapter_audio, 'book_name').' '.getProp($chapter_audio, 'chapter_name').'【'.getProp($chapter_audio, 'version_name').'】';
  425. }else {
  426. $task_name = getProp($chapter_audio, 'book_name').' '.getProp($chapter_audio, 'chapter_name').'【'.getProp($chapter_audio, 'version_name').'】('.($count+1).')';
  427. }
  428. $boolen1 = DB::table('mp_chapter_audios')->where('bid', $bid)->where('version_id', $version_id)->where('cid', $cid)->update(['generate_status'=>'制作中', 'generate_json' => $generate_json, 'updated_at' => date('Y-m-d H:i:s')]);
  429. if (!$boolen1) {
  430. DB::rollBack();
  431. Utils::throwError('20003:更新生成数据失败');
  432. }
  433. $id = DB::table('mp_audio_tasks')->insertGetId([
  434. 'audio_id' => getProp($chapter_audio, 'id'),
  435. 'generate_status' => '执行中',
  436. 'generate_json' => $generate_json,
  437. 'bid' => $bid,
  438. 'book_name' => getProp($chapter_audio, 'book_name'),
  439. 'version_id' => $version_id,
  440. 'version_name' => getProp($chapter_audio, 'version_name'),
  441. 'cid' => $cid,
  442. 'chapter_name' => getProp($chapter_audio, 'chapter_name'),
  443. 'task_name' => $task_name,
  444. 'created_at' => date('Y-m-d H:i:s'),
  445. 'updated_at' => date('Y-m-d H:i:s')
  446. ]);
  447. if (!$id) {
  448. DB::rollBack();
  449. Utils::throwError('20003:创建任务失败');
  450. }
  451. // // 删除章节分句音频数据并重新插入
  452. // DB::table('mp_chapter_paragraph_audios')->where('bid', $bid)->where('version_id', $version_id)->where('cid', $cid)->delete();
  453. // $boolen3 = DB::table('mp_chapter_paragraph_audios')->insert($mp_chapter_paragraph_audios);
  454. // if (!$boolen3) {
  455. // DB::rollBack();
  456. // Utils::throwError('20003:更新章节分句音频失败');
  457. // }
  458. } catch (\Exception $e) {
  459. DB::rollBack();
  460. Utils::throwError('20003:'.$e->getMessage());
  461. }
  462. DB::commit();
  463. // 通知火山生成音频
  464. $client = new Client(['timeout' => 300, 'verify' => false]);
  465. // 根据ID通过API通知合成音频
  466. // $result = $client->get("http://47.240.171.155:5000/api/chapterTask?taskId={$id}");
  467. $result = $client->get("http://122.9.129.83:5000/api/chapterTask?taskId={$id}");
  468. $response = $result->getBody()->getContents();
  469. $response_arr = json_decode($response, true);
  470. if (!isset($response_arr['code']) || (int)$response_arr['code'] !== 0) {
  471. $error_msg = isset($response_arr['msg']) ? $response_arr['msg'] : '未知错误';
  472. Log::info('通知火山生成音频失败: '.$error_msg);
  473. Utils::throwError('20003:通知火山生成音频失败');
  474. }
  475. return true;
  476. }
  477. public function timbreList($data) {
  478. $gender = getProp($data, 'gender');
  479. $timbre_name = getProp($data, 'voice_name');
  480. $category_id = getProp($data, 'category_id');
  481. $query = DB::table('mp_timbres')->where('is_enabled', 1)->select('timbre_name as voice_name', 'timbre_type as voice_type', 'gender', 'language', 'emotion', 'label', 'first_category_id', 'first_category_name', 'second_category_id', 'second_category_name', 'third_category_id', 'third_category_name', 'audio_url');
  482. if ($gender) {
  483. $query->where('gender', $gender);
  484. }
  485. if ($timbre_name) {
  486. $query->where('timbre_name', 'like', "%{$timbre_name}%");
  487. }
  488. if ($category_id) {
  489. $query->where(function ($query) use ($category_id) {
  490. $query->where('first_category_id', $category_id)->orWhere('second_category_id', $category_id)->orWhere('third_category_id', $category_id);
  491. });
  492. }
  493. $list = $query->get()->map(function ($value) {
  494. $value = (array)$value;
  495. $value['voice_name'] = str_replace('(多情感)', '', $value['voice_name']);
  496. return $value;
  497. })->toArray();
  498. return $list;
  499. }
  500. // 生成火山临时token
  501. public function setStsToken() {
  502. // ************* 配置参数 *************
  503. $method = 'GET';
  504. $service = 'sts';
  505. $host = 'open.volcengineapi.com';
  506. $region = env('VOLC_REGION');
  507. $endpoint = 'https://open.volcengineapi.com';
  508. // $endpoint = 'https://tos-cn-beijing.volces.com';
  509. $access_key = env('VOLC_AK');
  510. $secret_key = env('VOLC_SK');
  511. // 获取缓存中的token,如果没有则请求接口
  512. $token = Redis::get('volc_sts_token');
  513. if (!$token) {
  514. // 查询参数
  515. $query_parameters = [
  516. 'Action' => 'AssumeRole',
  517. 'RoleSessionName' => 'user@zw',
  518. 'RoleTrn' => 'trn:iam::2102575520:role/tos_role',
  519. 'Version' => '2018-01-01'
  520. ];
  521. // 生成URL编码的查询字符串
  522. $request_parameters = http_build_query($query_parameters, '', '&', PHP_QUERY_RFC3986);
  523. // 获取签名头信息
  524. $headers = $this->getSignHeaders($method, $service, $host, $region, $request_parameters, $access_key, $secret_key);
  525. // 构建完整URL
  526. $request_url = $endpoint . '?' . $request_parameters;
  527. $client = new Client(['verify' => false]);
  528. $response = $client->get($request_url, ['headers' => $headers]);
  529. $response_arr = json_decode($response->getBody()->getContents(), true);
  530. $result = [
  531. 'SessionToken' => isset($response_arr['Result']['Credentials']['SessionToken']) ? $response_arr['Result']['Credentials']['SessionToken'] : '',
  532. 'AccessKeyId' => isset($response_arr['Result']['Credentials']['AccessKeyId']) ? $response_arr['Result']['Credentials']['AccessKeyId'] : '',
  533. 'SecretAccessKey' => isset($response_arr['Result']['Credentials']['SecretAccessKey']) ? $response_arr['Result']['Credentials']['SecretAccessKey'] : '',
  534. 'Region' => env('VOLC_REGION'),
  535. 'Endpoint' => env('VOLC_END_POINT'),
  536. 'Bucket' => env('VOLC_BUCKET'),
  537. ];
  538. // 缓存token
  539. Redis::setex('volc_sts_token', 3000, json_encode($result));
  540. return $result;
  541. } else {
  542. return json_decode($token, true);
  543. }
  544. // $response = $response['Response'];
  545. // $access_key = $response['Credentials']['AccessKeyId'];
  546. // $secret_key = $response['Credentials']['AccessKeySecret'];
  547. // $security_token = $response['Credentials']['SecurityToken'];
  548. // $expiration = $response['Credentials']['Expiration'];
  549. // dd($response_arr);
  550. }
  551. public function emotionGroups($data) {
  552. $id = getProp($data, 'group_id');
  553. $group_name = getProp($data, 'group_name');
  554. $voice_type = getProp($data, 'voice_type');
  555. $query = DB::table('mp_emotion_groups')->where('is_enabled', 1)->select('id as group_id', 'group_name',
  556. 'emotion', 'emotion_type', 'voice_name', 'voice_type', 'speed_ratio', 'loudness_ratio', 'emotion_scale',
  557. 'pitch', 'generate_status', 'audio_url', 'error_msg');
  558. if ($group_name) {
  559. $query->where('group_name', 'like', "%{$group_name}%");
  560. }
  561. if ($id) {
  562. $query->where('id', $id);
  563. }
  564. if ($voice_type) {
  565. $query->where('voice_type', $voice_type);
  566. }
  567. return $query->orderBy('id')->get()->map(function ($value) {
  568. return (array)$value;
  569. })->toArray();
  570. }
  571. private function sign($key, $msg) {
  572. return hash_hmac('sha256', $msg, $key, true);
  573. }
  574. // 生成签名密钥
  575. private function getSignatureKey($key, $dateStamp, $regionName, $serviceName) {
  576. $kDate = $this->sign($key, $dateStamp);
  577. $kRegion = $this->sign($kDate, $regionName);
  578. $kService = $this->sign($kRegion, $serviceName);
  579. $kSigning = $this->sign($kService, 'request');
  580. return $kSigning;
  581. }
  582. // 获取签名头信息
  583. private function getSignHeaders($method, $service, $host, $region, $request_parameters, $access_key, $secret_key) {
  584. $contenttype = 'application/x-www-form-urlencoded';
  585. $accept = 'application/json';
  586. // 获取当前UTC时间
  587. $t = new DateTime('now', new DateTimeZone('UTC'));
  588. $xdate = $t->format('Ymd\THis\Z');
  589. $datestamp = $t->format('Ymd');
  590. // 1. 拼接规范请求串
  591. $canonical_uri = '/';
  592. $canonical_querystring = $request_parameters;
  593. $canonical_headers = "content-type:{$contenttype}\nhost:{$host}\nx-date:{$xdate}\n";
  594. $signed_headers = 'content-type;host;x-date';
  595. // 空请求体的SHA256哈希
  596. $payload_hash = hash('sha256', '');
  597. $canonical_request = implode("\n", [
  598. $method,
  599. $canonical_uri,
  600. $canonical_querystring,
  601. $canonical_headers,
  602. $signed_headers,
  603. $payload_hash
  604. ]);
  605. // 2. 拼接待签名字符串
  606. $algorithm = 'HMAC-SHA256';
  607. $credential_scope = implode('/', [$datestamp, $region, $service, 'request']);
  608. $hashed_canonical_request = hash('sha256', $canonical_request);
  609. $string_to_sign = implode("\n", [
  610. $algorithm,
  611. $xdate,
  612. $credential_scope,
  613. $hashed_canonical_request
  614. ]);
  615. // 3. 计算签名
  616. $signing_key = $this->getSignatureKey($secret_key, $datestamp, $region, $service);
  617. $signature = hash_hmac('sha256', $string_to_sign, $signing_key);
  618. // 4. 添加签名到请求头
  619. $authorization_header = sprintf(
  620. '%s Credential=%s/%s, SignedHeaders=%s, Signature=%s',
  621. $algorithm,
  622. $access_key,
  623. $credential_scope,
  624. $signed_headers,
  625. $signature
  626. );
  627. return [
  628. 'Accept' => $accept,
  629. 'Content-Type' => $contenttype,
  630. 'X-Date' => $xdate,
  631. 'Authorization' => $authorization_header
  632. ];
  633. }
  634. // 文字合成语音(火山引擎)
  635. public function tts($data) {
  636. $url = 'https://openspeech.bytedance.com/api/v1/tts';
  637. $headers = [
  638. 'Authorization' => 'Bearer;'.env('VOLC_TOKEN'),
  639. 'Content-Type' => 'application/json; charset=UTF-8'
  640. ];
  641. $post_data = [
  642. 'app' => [
  643. 'appid' => env('VOLC_APPID'),
  644. 'token' => env('VOLC_TOKEN'),
  645. 'cluster' => 'volcano_tts'
  646. ],
  647. 'user' => [
  648. 'uid' => 'mp_audio'
  649. ],
  650. // 'audio' => [
  651. // 'voice_type' =>
  652. // ],
  653. ];
  654. }
  655. public function generateScriptWords($cid, $model = 'r1') {
  656. ini_set('max_execution_time', 0);
  657. if (!$cid) Utils::throwError('20003: 请选择章节!');
  658. $content = DB::table('chapters as c')->leftJoin('chapter_contents as cc', 'c.chapter_content_id', '=', 'cc.id')->where('c.id', $cid)->value('cc.content');
  659. $model = $model == 'r1' ? 'deepseek-reasoner' : 'deepseek-chat';
  660. $messages = [
  661. [
  662. 'role' => 'system',
  663. 'content' => '下面有一段小说文本,请帮我将文本中的每句话按从上到下的顺序拆分成角色不同的剧本文稿(不得更改上下文顺序和内容),文稿形式严格按照“角色名(男或女):台词{情感}”输出,需要注意以下几点要求:
  664. 1.角色名后不要加入任何其他词语,只能加不包括旁白的性别,在男或女中选
  665. 2.非对话部分请全部用旁白角色代替
  666. 3.情感必须在【开心、悲伤、生气、惊讶、恐惧、厌恶、激动、冷漠、中性、沮丧、撒娇、害羞、安慰鼓励、咆哮、温柔、自然讲述、情感电台、磁性、广告营销、气泡音、新闻播报、娱乐八卦】中选一个,不得使用其他词语'
  667. ],
  668. [
  669. 'role' => 'user',
  670. 'content' => $content
  671. ]
  672. ];
  673. $post_data = [
  674. 'model' => $model, // R1模型: deepseek-reasoner V3模型: deepseek-chat
  675. 'messages' => $messages,
  676. 'max_tokens' => 8192,
  677. 'temperature' => 1, // 采样温度,介于 0 和 2 之间。更高的值,如 0.8,会使输出更随机,而更低的值,如 0.2,会使其更加集中和确定。 我们通常建议可以更改这个值或者更改 top_p,但不建议同时对两者进行修改。
  678. // 'top_p' => 1, // 作为调节采样温度的替代方案(<=1),模型会考虑前 top_p 概率的 token 的结果。所以 0.1 就意味着只有包括在最高 10% 概率中的 token 会被考虑。 我们通常建议修改这个值或者更改 temperature,但不建议同时对两者进行修改。
  679. 'frequency_penalty' => 0, // 介于 -2.0 和 2.0 之间的数字。如果该值为正,那么新 token 会根据其在已有文本中的出现频率受到相应的惩罚,降低模型重复相同内容的可能性。
  680. 'presence_penalty' => 0, // 介于 -2.0 和 2.0 之间的数字。如果该值为正,那么新 token 会根据其是否已在已有文本中出现受到相应的惩罚,从而增加模型谈论新主题的可能性。
  681. 'response_format' => [
  682. 'type' => 'text' // 默认值text,回答的结果输出文字(非接口返回值是text,接口返回值还是json字串),还可选:json_object,输出json格式
  683. ],
  684. 'stream' => false // 是否流式输出,如果设置为 True,将会以 SSE(server-sent events)的形式以流式发送消息增量。消息流以 data: [DONE] 结尾。
  685. ];
  686. $client = new Client(['timeout' => 1200, 'verify' => false]);
  687. $result = $client->post($this->url, ['json' => $post_data, 'headers' => $this->headers]);
  688. $response = $result->getBody()->getContents();
  689. $response_arr = json_decode($response, true);
  690. $update_data = [];
  691. $content = '';
  692. if (isset($response_arr['choices']) && count($response_arr['choices']) > 0) {
  693. $content = isset($response_arr['choices'][0]['message']['content']) ? $response_arr['choices'][0]['message']['content'] : '';
  694. $update_data = [
  695. 'role' => 'assistant',
  696. 'content' => $response_arr['choices'][0]['message']['content'],
  697. 'usage' => isset($response_arr['usage']) ? $response_arr['usage'] : []
  698. ];
  699. }
  700. return $content;
  701. }
  702. }