TestCommand.php 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880
  1. <?php
  2. namespace App\Console\Test;
  3. use App\Cache\UserCache;
  4. use App\Facade\Site;
  5. use App\Libs\TikTok\Kernel\Support\Str;
  6. use App\Models\MpGeneratePicTask;
  7. use App\Services\AIGeneration\AIImageGenerationService;
  8. use App\Services\DeepSeek\DeepSeekService;
  9. use getID3;
  10. use GuzzleHttp\Client;
  11. use Illuminate\Console\Command;
  12. use Illuminate\Support\Facades\DB;
  13. use Illuminate\Support\Facades\Redis;
  14. use Illuminate\Support\Facades\Storage;
  15. use Vinkla\Hashids\Facades\Hashids;
  16. use function PHPSTORM_META\map;
  17. /**
  18. * 测试
  19. */
  20. class TestCommand extends Command
  21. {
  22. /**
  23. * @var string
  24. */
  25. protected $signature = 'test {--token=} {--uid=} {--eid=} {--balance}';
  26. /**
  27. * The console command description.
  28. * php artisan Payment:BasePayment --bid='1'
  29. *
  30. * @var string
  31. */
  32. protected $description = '测试';
  33. protected $deepseekService;
  34. protected $AIImageGenerationService;
  35. public function __construct(DeepSeekService $deepseekService, AIImageGenerationService $AIImageGenerationService
  36. )
  37. {
  38. parent::__construct();
  39. $this->deepseekService = $deepseekService;
  40. $this->AIImageGenerationService = $AIImageGenerationService;
  41. }
  42. /**
  43. * handle
  44. */
  45. public function handle()
  46. {
  47. // 记录 DeepSeek 官方余额快照:php artisan test --balance
  48. if ($this->option('balance')) {
  49. $this->recordDeepSeekBalance();
  50. return;
  51. }
  52. // 测试视频超分功能
  53. // 使用示例:php artisan test --video_url=https://your-video-url.mp4
  54. $videoUrl = 'https://zw-audiobook.tos-cn-beijing.volces.com/video/ai_generation_1785144404_6a672454cabb0.mp4';
  55. if ($videoUrl) {
  56. $result = $this->testVideoEnhance($videoUrl);
  57. dd($result);
  58. return;
  59. }
  60. dd('end');
  61. $str = "【镜头1】
  62. 场景:@{顶层办公室}
  63. 画面:[大远景,冷蓝调夜景] 城市夜色如墨,高楼灯火如星点散落。落地窗占据画面主体,窗外城市天际线在夜色中延伸,室内灯光在玻璃上映出暖黄光晕。
  64. 运镜:缓慢推进,从城市全景推向落地窗,玻璃上逐渐映出室内轮廓。
  65. 配音台词:旁白:夜色压着整座城市,落地窗外高楼林立,灯火一片。
  66. 背景音效:城市远处微弱的车流声,空调低沉的嗡鸣";
  67. $new_str = str_replace(["@", "{", "}"], "", $str);
  68. dd($new_str);
  69. $a = microtime(true);
  70. $url = 'https://zw-audiobook.tos-cn-beijing.volces.com/image/ai_generation_1780411047_6a1eeaa72e372.jpg';
  71. $params = [
  72. 'asset_type' => 'Image',
  73. 'url' => $url,
  74. ];
  75. $result = $this->AIImageGenerationService->createAsset($params);
  76. $asset_id = getProp($result['data'], 'asset_id');
  77. dd($asset_id);
  78. $roles = DB::table('mp_animes')->where('id', 168)->value('roles');
  79. $roles = json_decode($roles, true);
  80. $count = 0;
  81. foreach ($roles as $item) {
  82. $count++;
  83. $a = microtime(true);
  84. $url = getProp($item, 'url');
  85. $role = getProp($item, 'role');
  86. if (!$url) continue;
  87. $params = [
  88. 'asset_type' => 'Image',
  89. 'url' => $url,
  90. ];
  91. $result = $this->AIImageGenerationService->createAsset($params);
  92. $asset_id = getProp($result['data'], 'asset_id');
  93. if ($asset_id) {
  94. $timeout = 300;
  95. $start = 3;
  96. while ($start < $timeout) {
  97. $result = $this->AIImageGenerationService->getAsset($asset_id);
  98. $code = getProp($result, 'code');
  99. if ((int)$code === 0) {
  100. $data = getProp($result, 'data', []);
  101. if ($data) {
  102. $asset = getProp($data, 'asset');
  103. if ($asset) {
  104. $status = getProp($asset, 'status');
  105. if (in_array($status, ['Active', 'Failed'])) {
  106. break;
  107. }
  108. }
  109. }
  110. }
  111. $start += 3;
  112. sleep(3);
  113. }
  114. }
  115. $b = microtime(true);
  116. dump("第{$count}次->".'{'.$role.'}执行时间: '.round($b-$a, 2));
  117. }
  118. dd('end');
  119. exit();
  120. $a = '{"content": "###第1集:废弃的真心\n\n###故事梗概\n内容梗概:\n 场次1 日 内 县城豪华酒店宴会厅\n 出场角色:林寒,宋筱薇,姜泽晨,两个孩子(大宝、小宝),宋家亲戚(岳父母、小姨子等)\n\n △县城最好的酒店,宴会厅内高朋满座,热闹非凡。\n △林寒穿着朴素的旧西服,像个佣人一样在桌间穿梭,不停地给宋家人端茶、倒酒、递烟。\n △主桌上,岳父母、小姨子等各路亲戚谈笑风生,没有人给林寒留座。\n △宴会致辞环节,林寒终于站到台上,拿起话筒。\n △他刚要开口,酒店大门“砰”地一声被猛地推开。\n △林寒抬头,瞬间血液凝固。\n △死去十八年的妻子宋筱薇,挽着同样死了的白月光姜泽晨,衣着光鲜,笑眯眯地站在门口。\n △林寒呆立当场,话筒里传来刺耳的蜂鸣。\n △宋筱薇走到他面前,语气轻蔑。\n 宋筱薇:林寒,十八年前咱俩没办离婚手续。\n 宋筱薇:这次回来,就是想补个手续,我要嫁给姜泽晨。\n 宋筱薇:两个儿子,以后也就不劳你费心了!\n △姜泽晨站在一旁,一脸得意。\n △宋家亲戚闻言,纷纷附和,说林寒任务完成,该退场了。\n △两个孩子懵了,紧紧拉着林寒的衣角,小声喊着“爸”。\n △所有人都盯着林寒,等着看他崩溃、哭闹、发疯。\n △林寒看着这一张张虚伪的脸,笑了。\n △心里想:等了十八年的戏,终于开场了。\n\n###美术风格\n基础画风风格词: 风格说明:经典二次元画风,线条干净、色彩明亮、人物精致柔和\n正向提示词(中文)\n日系动漫风格,线条清晰流畅,色彩明亮柔和,光影细腻,人物五官精致,整体二次元质感,画面干净清晰,风格统一稳定\n必须遵守规则\n• 全程保持日系动漫风格,不混入其他画风\n• 角色形象、五官、发型、服饰前后统一,不崩脸、不变形\n• 画面适配所选画面比例,构图居中,主体突出\n• 线条干净、色彩统一,不杂乱、不突兀\n禁止/避免词汇:水印、文字、logo、签名、模糊、低画质、畸形、多手、多脚、写实风格、水墨风格、3D 风格、杂乱背景、血腥、暴露、诡异表情\n\n###主体列表\n林寒:隐忍付出的倒插门女婿,十八年如一日抚养妻子的私生子,内心压抑但坚韧不拔,是一个为责任牺牲自我的悲剧性人物。{日系动漫风格,线条清晰流畅,色彩明亮柔和。男性,约45岁左右,亚洲人,面部消瘦,眼窝深陷透露出疲惫,眼角有深深的细纹,嘴唇干裂。身形偏瘦,微微佝偻。穿着洗得发白的旧款深蓝色西装,袖口磨得发亮,内搭一件领口松垮的白色衬衫。} {{中年男性(约40-45岁),音色低沉、沙哑,带有长年沉默的压抑感,语速缓慢,透露出疲惫与隐忍,但在关键时刻语气中带有决绝的凉意。}}\n宋筱薇:冷漠自私的前妻,十八年前假死与白月光私奔,如今风光归来,视林寒为工具,目标明确且无情。{日系动漫风格,线条清晰流畅,色彩明亮柔和。女性,约40岁左右,亚洲人,肤白貌美,面容精致,眼角微挑,神态高傲,带有金钱堆砌出的精致感。身形苗条,穿着剪裁合体的高级白色连衣裙,佩戴珍珠耳环。} {{成年女性(约35-40岁),音色清亮、柔美,但语气中带着居高临下的轻蔑与冷漠,语速不紧不慢,显得从容且虚伪。}}\n姜泽晨:得意忘形的白月光,宋筱薇的情人,性格骄傲且富有攻击性,享受着打败林寒的胜利感。{日系动漫风格,线条清晰流畅,色彩明亮柔和。男性,约40岁左右,亚洲人,面容俊朗,皮肤保养得当,发型打理得一丝不苟,嘴角常挂着一抹得意的笑。身形挺拔,穿着昂贵的深灰色定制西装,手腕上戴着名表。} {{成年男性(约35-40岁),音色偏高,语气带着明显的得意与挑衅,语调浮夸,透着一股小人得志的轻浮感。}}\n大宝:紧张不安的私生子之一,性格内向懵懂,对养父林寒有深厚且依赖的感情,面临身份冲击时本能地寻求父亲的保护。{日系动漫风格,线条清晰流畅,色彩明亮柔和。男性,约18岁左右,亚洲人,面容清秀,戴着一副黑框眼镜,眼神中带着学生特有的青涩与茫然。身形瘦高,穿着整洁的蓝色校服西装。} {{少年男性(约17-18岁),音色清澈且紧张,语气中带着不确定的颤抖,小声说话时透露出惶恐与依赖。}}\n小宝:紧张不安的私生子之一,性格沉默内向,比起语言更擅长用行动表达依赖,与哥哥一同视林寒为父亲。{日系动漫风格,线条清晰流畅,色彩明亮柔和。男性,约18岁左右,亚洲人,面容与哥哥相似,但眼神更显沉静,头发有些乱。身形瘦高,穿着与哥哥相同的整洁的蓝色校服西装。} {{少年男性(约17-18岁),音色低沉,话语较少,语气简短而坚定,带着压抑情绪的紧张感。}}\n宋岳母:势利刻薄的宋家大家长,代表着宋家所有人的利益与立场,对林寒毫无尊重与同情。{日系动漫风格,线条清晰流畅,色彩明亮柔和。女性,约65岁左右,亚洲人,面容保养得当但一脸尖酸相,头发盘得一丝不苟,眼神精明势利。身形微胖,穿着喜庆但俗气的暗红色绣花唐装。} {{老年女性(约60-65岁),音色尖锐,嗓门大,语气刻薄且不耐烦,透着一股不容置疑的压迫感。}}\n\n###场景列表\n宴会厅主舞台:豪华酒店宴会厅的中心区域,舞台上挂着“热烈祝贺林大宝、林小宝同学金榜题名”的红色横幅,灯光聚焦,后方有巨大的电子显示屏。{日系动漫风格,线条清晰流畅,色彩明亮柔和。室内场景,宽敞明亮的宴会厅中央高耸的舞台,红色横幅占据视觉中心,后方电子屏滚动播放着喜庆的字幕,两侧摆放着花篮,舞台灯光温暖明亮,洒在红色地毯上。无人物。}\n宴会厅主桌:位于舞台正前方,是一张巨大的圆桌,铺着洁白的桌布,摆满了丰盛的菜肴、高档白酒与红酒杯,是宋家亲属的席位。{日系动漫风格,线条清晰流畅,色彩明亮柔和。室内场景,一张巨大的铺着白桌布的圆桌,桌面上摆满精致的菜肴,中央是精致的玻璃转盘,上面放着酒瓶和酒杯。椅子是红色的高背椅,餐桌上方悬挂着华丽的吊灯,光线聚焦在美食上。无人物。}\n酒店大门入口:巨大的双开玻璃大门,门上贴着“喜”字窗花,门外是明亮的走廊,室内外光线在此处形成鲜明的明暗对比。{日系动漫风格,线条清晰流畅,色彩明亮柔和。室内通往室外的门界,两扇巨大的玻璃大门半开,门上贴着红色的“喜”字剪纸,门外是明亮的走廊,地面铺着大理石瓷砖。门框与门把手在灯光下反射着金属光泽。无人物。}\n\n###分段剧本\n片段数量:8\n旁白音色:中年男性(约40-45岁),音色低沉沙哑,带有长年隐忍带来的磨砺感,语速缓慢而沉稳,透着一股看透人情冷暖的沧桑与凉薄。\n\n##片段1\n时长:10s\n分镜1\n场景:宴会厅主桌\n画面:[近景,肩部以上,暖色调]林寒穿着洗得发白的旧西装,微微弓着背,手里拿着一瓶白酒,恭敬地为一位中年妇女倒酒。镜头侧拍,能看到他粗糙的手指紧紧握住瓶颈。\n运镜:固定镜头,从侧面拍摄林寒倒酒的动作。\n配音台词:\n背景音效:酒杯碰撞声,嘈杂的交谈声\n\n分镜2\n场景:宴会厅主桌\n画面:[特写,仰拍]岳母宋岳母坐在主位,翘着二郎腿,下巴微抬,眼神轻蔑地往下看,根本无视站在一旁倒酒的林寒。她手里的红酒杯在空中轻晃。\n运镜:从林寒的视角仰拍岳母,镜头微微晃动。\n配音台词:\n背景音效:嘈杂的说话声\n\n分镜3\n场景:宴会厅主桌\n画面:[中景,俯拍]林寒端着盘子,上面放着几包打开的中华烟,他在主桌前挨个递烟。一位肥头大耳的亲戚摆摆手,连腰都没弯一下,示意他继续。\n运镜:俯拍镜头,跟随林寒移动的步伐。\n配音台词:\n背景音效:椅子拖动声,人声嘈杂\n\n##片段2\n时长:12s\n分镜4\n场景:宴会厅主舞台\n画面:[全景,对称构图]宴会厅灯光明亮,红色横幅高悬。林寒独自站在舞台中央的话筒前,他双手握住话筒,指节泛白。台下人头攒动,但主桌那一块格外喧闹。\n运镜:从台下推向台上的林寒,镜头缓慢推进。\n配音台词:\n背景音效:喧闹声渐弱,话筒发出轻微的嗡鸣声\n\n##片段3\n时长:10s\n分镜5\n场景:酒店大门入口\n画面:[全景,逆光]宴会厅大门“砰”地一声被猛地推开。门口的光线刺眼,两个人形的剪影逆光站在门口,轮廓鲜明,衣着光鲜。他们的身形在满堂酒席的暖光背景下显得格格不入。\n运镜:镜头猛地聚焦到门口,快速推进,定格在剪影上。\n配音台词:\n背景音效:沉重的吱呀推门声,随后全场瞬间安静\n\n##片段4\n时长:12s\n分镜6\n场景:宴会厅主舞台\n画面:[特写,大特写]林寒的脸部特写。他嘴唇微张,瞳孔骤缩,脸上松弛的线条瞬间紧绷,仿佛看到了极其不可思议的景象,鲜血凝固的感觉。\n运镜:从全景极速推到林寒的面部特写,形成强烈的视觉冲击。\n配音台词:中文配音:[旁白]我死去十八年的老婆宋筱薇,挽着同样死去的白月光姜泽晨,笑眯眯地站在门口。\n背景音效:心跳声陡然加大\n\n##片段5\n时长:15s\n分镜7\n场景:宴会厅主舞台\n画面:[中景,三人构图]宋筱薇穿着白色连衣裙,挽着姜泽晨的胳膊,踩着高跟鞋优雅地走到林寒面前。她面带微笑,眼神却冰冷。姜泽晨站在一侧,双手插兜,嘴角挂着得意的弧度。\n运镜:从舞台侧面平移,跟随宋筱薇和姜泽晨移动,最终将林寒框入镜头。\n配音台词:中文配音:[宋筱薇]林寒,十八年前咱俩没办离婚手续。\n背景音效:高跟鞋踩地声,清脆\n\n分镜8\n场景:宴会厅主舞台\n画面:[近景]镜头聚焦在宋筱薇的脸上。她微微歪头,笑容变得轻蔑,用一种命令式的语气接着说。\n运镜:缓慢推进到宋筱薇的面部特写。\n配音台词:中文配音:[宋筱薇]这次回来,就是想补个手续,我要嫁给姜泽晨。\n背景音效:\n\n分镜9\n场景:宴会厅主舞台\n画面:[近景]姜泽晨插话,头微微扬起,看着林寒,下巴高抬,眼神中带着胜利者的炫耀。背景里,台下的宋家亲戚开始窃窃私语。\n运镜:切到姜泽晨的正面近景,固定镜头。\n配音台词:中文配音:[姜泽晨]两个儿子,以后也就不劳你费心了!\n背景音效:台下议论声(嘈杂的嗡嗡声)\n\n##片段6\n时长:14s\n分镜10\n场景:宴会厅主舞台\n画面:[中景,长焦]两个孩子,大宝和小宝,穿着校服,一左一右紧紧挨着林寒。大宝的手攥着林寒的袖口,小宝则低着头,身体微微发抖。他们像受惊的小鸟,不敢抬头看面前的两个人。\n运镜:长焦镜头,从侧面拍摄,虚化背景的宋筱薇和姜泽晨,突出孩子的无助。\n配音台词:中文配音:[大宝/小宝合]爸。\n背景音效:\n\n分镜11\n场景:宴会厅主桌\n画面:[全景,广角]宋岳母“啪”地一拍桌子,站了起来,对着台上的林寒皱眉。其他宋家亲戚也交头接耳,指点着林寒,表情充满了不耐烦和驱赶的意味。\n运镜:快速摇到主桌,广角镜头囊括所有宋家人的反应。\n配音台词:中文配音:[宋岳母]林寒,你任务完成了,该退场了。\n背景音效:拍桌子的响声,椅子拖地声\n\n##片段7\n时长:8s\n分镜12\n场景:宴会厅主舞台\n画面:[特写,仰拍]林寒的脸。他先是低着头,刘海遮住了眼睛。然后,他缓缓抬起头,嘴角向上扯动,露出一个皮笑肉不笑的弧度。眼神里没有悲伤,没有愤怒,只有一种彻骨的凉意和释然。\n运镜:从低角度仰拍,跟随他抬头的动作,捕捉他嘴角的笑。\n配音台词:\n背景音效:\n\n##片段8\n时长:10s\n分镜13\n场景:宴会厅主舞台\n画面:[大特写]林寒的正面大特写,眼神直勾勾地看着镜头(观众),仿佛在与看客交流。他嘴角的笑纹加深,眼神中闪烁着看透一切的微光。\n运镜:极慢速向前推进,最终完全定格在他的眼睛上。\n配音台词:中文配音:[旁白]等了十八年的戏,终于开场了。\n背景音效:画面外的嘈杂声渐弱,最终只剩下心跳声", "anime_id": "197"}';
  121. $originalContent = json_decode($a, true)['content'];
  122. $episodes = handleEpisodeContentForAce($originalContent);
  123. dd($episodes);
  124. $this->handleHistoryTasks();
  125. $this->handleHistorySegments();
  126. dd('end!');
  127. // dd(testStoryboardParsing(''));
  128. $content = "";
  129. dd(handleScriptContent($content));
  130. exit();
  131. $url = 'https://zw-audiobook.tos-cn-beijing.volces.com/video/ai_generation_1772704798_69a9541e94bef';
  132. $video_name = 'ai_generation_' . time() . '_' . uniqid();
  133. $video_ext = getVideoExtFromUrl($url);
  134. $video_name = $video_name . $video_ext;
  135. dump($video_name);
  136. $video_url = uploadStreamByTos('video', $url, $video_name);
  137. dd($video_url);
  138. // $url = 'https://zw-audiobook.tos-cn-beijing.volces.com/image/ai_generation_1772525682_69a69872e3a87..png';
  139. // // $url = 'https://zw-audiobook.tos-cn-beijing.volces.com/image/ai_generation_1772525682_69a69872e3a87.png';
  140. // $compressed_url = compressRemoteImageUrlToSize($url);
  141. // // $compressed_url = compressRemoteImageUrlToSizeImagick($url);
  142. // $filename = storage_path('app/public/video_frames/compressed.png');
  143. // // $url = uploadStreamByTos('image', $compressed_url, $filename);
  144. // $url = file_put_contents($filename, $compressed_url);
  145. // dd($url);
  146. // // 转存阿里云oss文件到tos
  147. // $audio_effects = DB::table('mp_timbres')->where('id', 246)->get();
  148. // $total = count($audio_effects);
  149. // $success = 0;
  150. // $continue = 0;
  151. // $fail = 0;
  152. // foreach ($audio_effects as $item) {
  153. // $id = getProp($item, 'id');
  154. // $url = getProp($item, 'audio_url');
  155. // if (!$url) {
  156. // $continue++;
  157. // continue;
  158. // }
  159. // if (!strstr($url, 'https://cdn-zwai.ycsd.cn')) {
  160. // $continue++;
  161. // continue;
  162. // }
  163. // $index = strrpos($url, '/');
  164. // $filename = mb_substr($url, $index + 1);
  165. // $stream = file_get_contents($url);
  166. // $new_url = uploadStreamByTos('demonstrate', $stream, $filename);
  167. // dd($new_url);
  168. // if ($new_url) {
  169. // $boolen = DB::table('mp_timbres')->where('id', $id)->update([
  170. // 'audio_url' => $new_url,
  171. // 'updated_at' => date('Y-m-d H:i:s'),
  172. // ]);
  173. // if ($boolen) $success++;
  174. // else $fail++;
  175. // }else {
  176. // dump('未生成新url');
  177. // $fail++;
  178. // }
  179. // }
  180. // dd("总条数:{$total}, 成功条数:{$success}, 跳过条数:{$continue}, 失败条数:{$fail}");
  181. exit();
  182. // $audio_effects = DB::table('mp_bgms')->where('is_enabled', 1)->where('playtime_seconds', 0)->get();
  183. // foreach ($audio_effects as $audio_effect) {
  184. // $id = getProp($audio_effect, 'id');
  185. // $url = getProp($audio_effect, 'bgm_url');
  186. // $index = strrpos($url, '/');
  187. // $name = 'The_Dawn';
  188. // // 将远程地址文件保存到本地
  189. // $filename = storage_path().'/app/public/audio_effect/' . $name;
  190. // if (!file_exists($filename)) {
  191. // $file = file_get_contents($url);
  192. // file_put_contents($filename, $file);
  193. // }
  194. // $getID3 = new getID3();
  195. // $ThisFileInfo = $getID3->analyze($filename);
  196. // $seconds = floor($ThisFileInfo['playtime_seconds']) ?? 0;
  197. // if ($seconds <= 0) $seconds = 1;
  198. // DB::table('mp_bgms')->where('id', $id)->update([
  199. // 'playtime_seconds' => $seconds,
  200. // 'updated_at' => date('Y-m-d H:i:s'),
  201. // ]);
  202. // unlink($filename);
  203. // }
  204. // exit();
  205. // $wavPath = public_path('audio');
  206. // if (!file_exists($wavPath)) {
  207. // return [];
  208. // }
  209. // $files = scandir($wavPath);
  210. // $error_voice = [];
  211. // foreach ($files as $file) {
  212. // if ($file !== '.' && $file !== '..') {
  213. // $voice_type = pathinfo($file, PATHINFO_FILENAME);
  214. // $boolen = DB::table('mp_timbres')->where('timbre_type', $voice_type)->update([
  215. // 'audio_url' => 'https://cdn-zwai.ycsd.cn/mp_audio/voice_demo/' . $file,
  216. // 'is_enabled' => 1,
  217. // 'updated_at' => date('Y-m-d H:i:s'),
  218. // ]);
  219. // if (!$boolen) $error_voice[] = $file;
  220. // }
  221. // }
  222. // \Log::info('错误文件:' . json_encode($error_voice, 256));
  223. // exit();
  224. // exit();
  225. $bid = 321;
  226. $version_id = 1663;
  227. $cid = 708197;
  228. $redis_key = "select-{$bid}-{$version_id}-{$cid}";
  229. // 查询更新后的信息
  230. $paragraph_urls = DB::table('mp_chapter_paragraph_audios')->where('bid', $bid)->where('version_id', $version_id)
  231. ->where('cid', $cid)->where(function($query) {
  232. return $query->where('generate_status', '!=', '制作中')->orWhere('error_msg', '!=', '');
  233. })->select('id', 'sequence', 'paragraph_audio_url', 'error_msg')
  234. ->get()->map(function ($value) {
  235. return (array)$value;
  236. })->toArray();
  237. $rem_ids = [];
  238. foreach ($paragraph_urls as $item) {
  239. $rem_ids[] = getProp($item, 'id');
  240. }
  241. // 删除已发送的id
  242. Redis::srem($redis_key, $rem_ids);
  243. dd(Redis::smembers($redis_key));
  244. exit();
  245. // $all_timbre = DB::table('mp_timbres')->get();
  246. // foreach($all_timbre as $timbre) {
  247. // $id = getProp($timbre, 'id');
  248. // if ($id > 1) {
  249. // $language = getProp($timbre, 'language');
  250. // $language = str_replace(['、', ','], ',', $language);
  251. // DB::table('mp_timbres')->where('id', $id)->update([
  252. // 'language' => $language,
  253. // 'first_category_id' => 0,
  254. // 'first_category_name' => '',
  255. // 'second_category_id' => 0,
  256. // 'second_category_name' => '',
  257. // 'third_category_id' => 0,
  258. // 'third_category_name' => '',
  259. // ]);
  260. // }
  261. // }
  262. // dd('end');
  263. ini_set('max_execution_time', 0);
  264. \Log::info('~~~~~~~~~~~~~~~~~~~~~~~~开始~~~~~~~~~~~~~~~~~~~~~~~~');
  265. // $a = DB::table('mp_emotion_list')->pluck('emotion_name')->toArray();
  266. // dd(implode('、', $a));
  267. // $emotion_str = '​开心(happy),悲伤(sad),生气(angry),惊讶(surprised),恐惧(fear),厌恶(hate),激动(excited),冷漠(coldness),中性(neutral),沮丧(depressed),撒娇(lovey-dovey),害羞(shy),安慰鼓励(comfort),咆哮/焦急(tension),温柔(tender),讲故事 / 自然讲述(storytelling),情感电台(radio),磁性(magnetic),广告营销(advertising),气泡音(vocal - fry),低语 (ASMR),新闻播报(news),娱乐八卦(entertainment),方言(dialect)';
  268. // $emotion_arr = explode(',', $emotion_str);
  269. // $insert_data = [];
  270. // foreach ($emotion_arr as $emotion) {
  271. // preg_match('/(.*)?((.*?))/', $emotion, $match);
  272. // if (count($match) == 3) {
  273. // $insert_data[] = [
  274. // 'emotion_code' => $match[2],
  275. // 'emotion_name' => $match[1],
  276. // 'is_enabled' => 1,
  277. // 'created_at' => date('Y-m-d H:i:s'),
  278. // 'updated_at' => date('Y-m-d H:i:s'),
  279. // ];
  280. // }
  281. // }
  282. // DB::table('mp_emotion_list')->insert($insert_data);
  283. // exit();
  284. $bid = 10917;
  285. $version_id = 1649;
  286. $cid = 2669608;
  287. // $text = '"旁白:刀尖刺穿手掌的瞬间,疼痛自掌心钻入骨髓。{自然讲述} \n旁白:心脏像是被万箭穿过一般,我痛得几近晕厥。{自然讲述} \n旁白:可赵晏池显然不愿让我那么痛快地昏过去。{自然讲述} \n旁白:他命人用一盆盆冰凉刺骨的冷水强行让我保持清醒。{自然讲述} \n旁白:血迹混杂着水迹在身下漫开,任谁看了都触目惊心。{自然讲述} \n旁白:意识涣散间,我仿佛在脑海中看到走马灯。{自然讲述} \n旁白:结婚七年,圈子里无人不知我是赵晏池宠在心尖上的人。{自然讲述} \n旁白:他会在拍卖会上一掷千金买下我随口说喜欢的东西。{自然讲述} \n旁白:他会在我生病的时候亲自衣不解带地照顾我。{自然讲述} \n旁白:从不落泪的男人,看到我身体难受吃不下饭时却心疼得湿了眼眶。{自然讲述} \n旁白:他会在求婚那天,包下国内外所有大荧幕,高调地向全世界宣布爱我。{自然讲述} \n旁白:那一夜,满城的天都被他准备的烟花照亮。{自然讲述} \n旁白:绚烂的烟火下,他深情款款地看着我的眼睛。{自然讲述} \n赵晏池(男):我就是要让所有人都知道,你是我赵晏池的夫人,是我的心头肉。{温柔} \n赵晏池(男):有我保护你,谁都别想动你一根手指头。{温柔} \n旁白:可偏偏如今,伤我最深的,就是给过我这些承诺的人。{自然讲述} \n旁白:压制着我的保镖终于将我松开。{自然讲述} \n旁白:我想摸摸肚子里的孩子,却根本抬不起手。{自然讲述} \n旁白:我的手已经被彻底废掉,绵软得使不上一点力气。{自然讲述} \n赵晏池(男):把她给我扔出去,别在这边脏了我们的眼睛。{冷漠} \n旁白:保镖正要照做,却又被旁边的女人制止。{自然讲述} \n汪思月(女):等等。{温柔} \n旁白:她声音轻柔,却像是恶鬼的轻语:{自然讲述} \n汪思月(女):晏池,我看他们也挺辛苦的。{温柔} \n汪思月(女):这个女的身材看着倒是不错,不如就赏给他们玩玩吧。{温柔} \n旁白:下一秒,赵晏池宠溺的声音响起。{自然讲述} \n赵晏池(男):思月,还是你小主意最多。{温柔} \n旁白:听到这个有些耳熟的名字,我努力抬起肿起的眼皮。{自然讲述} \n旁白:竟然是她。{自然讲述} \n旁白:汪思月,赵晏池的初恋。{自然讲述} \n旁白:刚在一起的时候我就听过这个名字。{自然讲述} \n旁白:可那时他信誓旦旦地和我保证,他们已经是过去式了。{自然讲述} \n旁白:他的心里只有我一个人。{自然讲述} \n旁白:不等我想太多,赵晏池下一句话像一道惊雷劈下来。{自然讲述} \n赵晏池(男):好,就听你的。{冷漠} \n旁白:他对那几个保镖淡然道:{自然讲述} \n赵晏池(男):她归你们了,别玩死,死在这儿太晦气。{冷漠} \n旁白:保镖的脸上露出欣喜而猥琐的笑容。{自然讲述} \n旁白:我拼命摇头,张开嘴想告诉他们我的真实身份。{自然讲述} \n旁白:可药物作用让我发不出一个完整的音节。{自然讲述} \n旁白:拼尽全力,我也只能呼出嗬哧嗬哧的气声。{自然讲述} \n旁白:他们摩拳擦掌着靠近我。{自然讲述} \n旁白:拖着死鱼一样的身体,我吃力地在地上匍匐,可这全然是徒劳。{自然讲述} \n旁白:很快,我便被他们按住,压在身下。{自然讲述} \n旁白:撕裂一般的疼痛占据了我的身体,生理性的恶心冲撞着我的五脏六腑。{自然讲述} \n旁白:不多时,小腹处传来一阵坠痛。{自然讲述} \n旁白:我眼睁睁看着自己身下晕开一片血迹。{自然讲述} \n旁白:我知道,我的孩子终究是没保住。{自然讲述} \n旁白:保镖愣了一瞬间后也反应过来。{自然讲述} \n保镖(男):这娘们儿还怀孕了?{惊讶} \n保镖(男):怀孕?{惊讶} \n旁白:赵晏池听到这两个字蹙起眉。{自然讲述} \n旁白:他似乎想到了什么,拿起手机发了条消息。{自然讲述} \n旁白:我放在袋子里的手机响起轻微的提示音,可惜没人听见。{自然讲述} \n旁白:他没有再多想:{自然讲述} \n赵晏池(男):你们继续吧。{冷漠} \n旁白:我无神地盯着天花板,泪水糊满了整张脸。{自然讲述} \n旁白:绝望将我包围,整个人都陷入深不见底的深渊。{自然讲述}"';
  288. // $text = '"旁白:刀尖刺穿手掌的瞬间,疼痛自掌心钻入骨髓。 \n旁白:心脏像是被万箭穿过一般,我痛得几近晕厥。 \n"';
  289. // $text = json_decode($text, true);
  290. // dd(handleScriptWords($text, 0));
  291. $text = $this->deepseekService->generateScriptWords($cid, 'r1');
  292. \Log::info('text: '.json_encode($text, 256));
  293. // $token = getTextTokens($text);
  294. $result = handleScriptWords($text);
  295. $role_gender = getProp($result, 'role_gender');
  296. $role_timbre = [];
  297. foreach ($role_gender as $role => $gender) {
  298. $timbre = DB::table('mp_timbres')->where('gender', $gender)->first();
  299. if (!$timbre) {
  300. $role_timbre[$role] = ['timbre_type' => 'zh_male_ruyayichen_emo_v2_mars_bigtts', 'timbre_name' => '儒雅男友'];
  301. }else {
  302. $timbre_name = str_replace('(多情感)', '', $timbre->timbre_name);
  303. $role_timbre[$role] = [
  304. 'timbre_type' => $timbre->timbre_type,
  305. 'timbre_name' => $timbre_name
  306. ];
  307. }
  308. }
  309. // 更新角色-音色信息
  310. $existed_role_info = DB::table('mp_book_version')->where('bid', $bid)->where('id', $version_id)->value('role_info');
  311. $existed_role_info = json_decode($existed_role_info, true);
  312. if ($existed_role_info) $existed_roles = array_keys($existed_role_info);
  313. else $existed_roles = [];
  314. foreach ($role_timbre as $role => $timbre) {
  315. if (!in_array($role, $existed_roles)) {
  316. $existed_role_info[$role] = $timbre;
  317. }
  318. }
  319. // 获取情感信息
  320. $emotion_list = DB::table('mp_emotion_list')->where('is_enabled', 1)->pluck('emotion_name', 'emotion_code')->toArray();
  321. $emotion_list = array_flip($emotion_list);
  322. // 构造生成音频的json
  323. $words = getProp($result, 'words');
  324. foreach($words as &$word) {
  325. $role = getProp($word, 'role');
  326. $word['gender'] = (int)$word['gender'];
  327. if (isset($emotion_list[getProp($word, 'emotion')])) { // 如果有对应情感则赋值,没有则默认为中性(neutral)
  328. $word['emotion_type'] = $emotion_list[getProp($word, 'emotion')];
  329. }else {
  330. $word['emotion'] = '中性';
  331. $word['emotion_type'] = 'neutral';
  332. }
  333. $word['voice_name'] = $role_timbre[$role]['timbre_name'];
  334. $word['voice_type'] = $role_timbre[$role]['timbre_type'];
  335. $word['speed_ratio'] = mt_rand(9,11)/10;
  336. $word['loudness_ratio'] = mt_rand(5,12)/10;
  337. $word['emotion_scale'] = mt_rand(1,5);
  338. }
  339. $generate_json = json_encode($words, 256);
  340. \Log::info('generate_json: '.$generate_json);
  341. try {
  342. DB::beginTransaction();
  343. $role_info = json_encode($existed_role_info, 256);
  344. $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')]);
  345. if (!$boolen) {
  346. DB::rollBack();
  347. dd('更新角色信息失败');
  348. }
  349. $count = DB::table('mp_audio_tasks')->where('bid', $bid)->where('version_id', $version_id)->where('cid', $cid)->count('id');
  350. $chapter_audio = DB::table('mp_chapter_audios')->where('bid', $bid)->where('version_id', $version_id)->where('cid', $cid)->first();
  351. if (!$count) {
  352. $task_name = getProp($chapter_audio, 'book_name').' '.getProp($chapter_audio, 'chapter_name').'【'.getProp($chapter_audio, 'version_name').'】';
  353. }else {
  354. $task_name = getProp($chapter_audio, 'book_name').' '.getProp($chapter_audio, 'chapter_name').'【'.getProp($chapter_audio, 'version_name').'】('.($count+1).')';
  355. }
  356. $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')]);
  357. if (!$boolen1) {
  358. DB::rollBack();
  359. dd('更新生成数据失败');
  360. }
  361. $boolen2 = DB::table('mp_audio_tasks')->insert([
  362. 'audio_id' => getProp($chapter_audio, 'id'),
  363. 'status' => '执行中',
  364. 'generate_json' => $generate_json,
  365. 'bid' => $bid,
  366. 'book_name' => getProp($chapter_audio, 'book_name'),
  367. 'version_id' => $version_id,
  368. 'version_name' => getProp($chapter_audio, 'version_name'),
  369. 'cid' => $cid,
  370. 'chapter_name' => getProp($chapter_audio, 'chapter_name'),
  371. 'task_name' => $task_name,
  372. 'created_at' => date('Y-m-d H:i:s'),
  373. 'updated_at' => date('Y-m-d H:i:s')
  374. ]);
  375. if (!$boolen2) {
  376. DB::rollBack();
  377. dd('创建任务失败');
  378. }
  379. } catch (\Exception $e) {
  380. DB::rollBack();
  381. dd('失败');
  382. }
  383. DB::commit();
  384. \Log::info('~~~~~~~~~~~~~~~~~~~~~~~~结束~~~~~~~~~~~~~~~~~~~~~~~~');
  385. dd('成功!');
  386. // \Log::info('generate_json: '.$generate_json);
  387. }
  388. /**
  389. * 记录 DeepSeek 官方余额快照(当前时间 + 余额),追加写入独立日志文件
  390. * 用法:php artisan test --balance
  391. */
  392. private function recordDeepSeekBalance()
  393. {
  394. $apiKey = env('DEEPSEEK_API_KEY');
  395. if (empty($apiKey)) {
  396. $this->error('DEEPSEEK_API_KEY 未配置');
  397. return;
  398. }
  399. try {
  400. $client = new Client(['timeout' => 30, 'verify' => false]);
  401. $response = $client->get('https://api.deepseek.com/user/balance', [
  402. 'headers' => [
  403. 'Authorization' => 'Bearer ' . $apiKey,
  404. 'Accept' => 'application/json',
  405. ],
  406. ]);
  407. $data = json_decode($response->getBody()->getContents(), true);
  408. $info = $data['balance_infos'][0] ?? [];
  409. $line = sprintf(
  410. "[%s] 当前余额:%s %s(充值余额:%s;赠送余额:%s),账户%s",
  411. date('Y-m-d H:i:s'),
  412. getProp($info, 'total_balance', '0.00'),
  413. getProp($info, 'currency', 'CNY') === 'CNY' ? '元' : getProp($info, 'currency', 'CNY'),
  414. getProp($info, 'topped_up_balance', '0.00'),
  415. getProp($info, 'granted_balance', '0.00'),
  416. !empty($data['is_available']) ? '可用' : '不可用'
  417. ) . PHP_EOL;
  418. $logPath = storage_path('logs/deepseek_balance.log');
  419. file_put_contents($logPath, $line, FILE_APPEND | LOCK_EX);
  420. $this->info('已记录 DeepSeek 余额快照: ' . trim($line));
  421. $this->info('日志文件: ' . $logPath);
  422. } catch (\Exception $e) {
  423. $this->error('DeepSeek 余额查询失败: ' . $e->getMessage());
  424. }
  425. }
  426. private function testVideoEnhance($video_url) {
  427. $startTime = microtime(true); // 开始计时
  428. $this->info("========== 开始测试视频超分 ==========");
  429. $this->info("原始视频URL: {$video_url}");
  430. try {
  431. // 获取API Key
  432. $apiKey = env('VOLC_AI_MEDIAKIT_KEY');
  433. if (empty($apiKey)) {
  434. $this->error('VOLC_AI_MEDIAKIT_KEY 未配置');
  435. return;
  436. }
  437. // 1. 提交超分任务
  438. $this->info("\n步骤1: 提交超分任务...");
  439. $submitStartTime = microtime(true);
  440. $client = new Client(['verify' => false, 'timeout' => 120]);
  441. $requestParams = [
  442. 'video_url' => $video_url,
  443. 'resolution' => '720p',
  444. 'bitrate_level' => 'medium'
  445. ];
  446. $this->info("请求参数: " . json_encode($requestParams, JSON_UNESCAPED_UNICODE));
  447. $response = $client->post('https://mediakit.cn-beijing.volces.com/api/v1/tools/enhance-video-fast', [
  448. 'headers' => [
  449. 'Authorization' => 'Bearer ' . $apiKey,
  450. 'Content-Type' => 'application/json',
  451. ],
  452. 'json' => $requestParams
  453. ]);
  454. $responseData = json_decode($response->getBody(), true);
  455. $submitEndTime = microtime(true);
  456. $submitDuration = round($submitEndTime - $submitStartTime, 2);
  457. $this->info("提交响应: " . json_encode($responseData, JSON_UNESCAPED_UNICODE));
  458. $this->comment("提交耗时: {$submitDuration}秒");
  459. if (!isset($responseData['success']) || $responseData['success'] !== true) {
  460. $errorMsg = isset($responseData['error']) ?
  461. ($responseData['error']['message'] ?? json_encode($responseData['error'])) :
  462. '未知错误';
  463. $this->error("任务提交失败: {$errorMsg}");
  464. return;
  465. }
  466. $taskId = $responseData['task_id'];
  467. $this->info("✓ 任务提交成功,任务ID: {$taskId}");
  468. // 2. 轮询查询任务状态
  469. $this->info("\n步骤2: 开始轮询查询任务状态...");
  470. $pollingStartTime = microtime(true);
  471. $maxAttempts = 120; // 最多轮询120次(10分钟)
  472. $interval = 5; // 每5秒查询一次
  473. $attempt = 0;
  474. while ($attempt < $maxAttempts) {
  475. $attempt++;
  476. $this->info("\n第 {$attempt} 次查询(间隔{$interval}秒)...");
  477. sleep($interval);
  478. try {
  479. $queryResponse = $client->get('https://mediakit.cn-beijing.volces.com/api/v1/tasks/' . $taskId, [
  480. 'headers' => [
  481. 'Authorization' => 'Bearer ' . $apiKey,
  482. ]
  483. ]);
  484. $queryData = json_decode($queryResponse->getBody(), true);
  485. $taskStatus = $queryData['status'] ?? 'unknown';
  486. $this->info("任务状态: {$taskStatus}");
  487. if ($taskStatus === 'completed') {
  488. $pollingEndTime = microtime(true);
  489. $pollingDuration = round($pollingEndTime - $pollingStartTime, 2);
  490. // 任务成功
  491. $this->info("\n========== 超分任务完成 ==========");
  492. $this->comment("轮询耗时: {$pollingDuration}秒 (查询{$attempt}次)");
  493. if (isset($queryData['result']['video_url'])) {
  494. $enhancedUrl = $queryData['result']['video_url'];
  495. $this->info("\n✓ 超分成功!API返回的临时URL:");
  496. $this->info($enhancedUrl);
  497. // 3. 上传到TOS
  498. $this->info("\n步骤3: 开始上传超分视频到TOS...");
  499. $uploadStartTime = microtime(true);
  500. try {
  501. // 生成文件名
  502. $videoName = 'enhanced_video_' . time() . '_' . uniqid() . '.mp4';
  503. // 下载并上传到TOS
  504. $this->comment("正在下载超分视频...");
  505. $videoContent = file_get_contents($enhancedUrl);
  506. $videoSize = strlen($videoContent);
  507. $this->comment("视频大小: " . round($videoSize / 1024 / 1024, 2) . " MB");
  508. $this->comment("正在上传到TOS...");
  509. $tosUrl = uploadStreamByTos('video', $videoContent, $videoName);
  510. $uploadEndTime = microtime(true);
  511. $uploadDuration = round($uploadEndTime - $uploadStartTime, 2);
  512. $this->info("✓ 上传成功!耗时: {$uploadDuration}秒");
  513. $this->info("TOS地址: {$tosUrl}");
  514. } catch (\Exception $e) {
  515. $this->error("✗ 上传TOS失败: " . $e->getMessage());
  516. $this->warn("将返回原始临时URL");
  517. $tosUrl = $enhancedUrl;
  518. $uploadDuration = 0;
  519. }
  520. // 计算总耗时
  521. $totalEndTime = microtime(true);
  522. $totalDuration = round($totalEndTime - $startTime, 2);
  523. // 显示详细信息
  524. $this->info("\n========== 执行完成 ==========");
  525. if (isset($queryData['result']['duration'])) {
  526. $this->info("视频时长: " . $queryData['result']['duration'] . "秒");
  527. }
  528. if (isset($queryData['result']['resolution'])) {
  529. $this->info("输出分辨率: " . $queryData['result']['resolution']);
  530. }
  531. if (isset($queryData['result']['fps'])) {
  532. $this->info("输出帧率: " . $queryData['result']['fps'] . " fps");
  533. }
  534. $this->info("\n========== 时间统计 ==========");
  535. $this->info("提交任务耗时: {$submitDuration}秒");
  536. $this->info("等待处理耗时: {$pollingDuration}秒 (查询{$attempt}次)");
  537. $this->info("上传TOS耗时: {$uploadDuration}秒");
  538. $this->info("总执行时间: {$totalDuration}秒");
  539. $this->info("\n========== URL对比 ==========");
  540. $this->info("原始视频URL: {$video_url}");
  541. $this->info("超分后TOS地址: {$tosUrl}");
  542. return [
  543. 'success' => true,
  544. 'original_url' => $video_url,
  545. 'enhanced_url' => $enhancedUrl, // API临时URL
  546. 'tos_url' => $tosUrl, // TOS永久URL
  547. 'task_id' => $taskId,
  548. 'result' => $queryData['result'],
  549. 'duration' => [
  550. 'submit' => $submitDuration,
  551. 'polling' => $pollingDuration,
  552. 'upload' => $uploadDuration,
  553. 'total' => $totalDuration
  554. ],
  555. 'polling_attempts' => $attempt
  556. ];
  557. } else {
  558. $this->error("✗ 任务完成但未找到视频URL");
  559. return;
  560. }
  561. } elseif ($taskStatus === 'failed') {
  562. // 任务失败
  563. $pollingEndTime = microtime(true);
  564. $pollingDuration = round($pollingEndTime - $pollingStartTime, 2);
  565. $totalDuration = round($pollingEndTime - $startTime, 2);
  566. $this->error("\n✗ 超分任务失败");
  567. $this->error("完整响应: " . json_encode($queryData, JSON_UNESCAPED_UNICODE));
  568. $this->info("\n总执行时间: {$totalDuration}秒 (轮询{$attempt}次,耗时{$pollingDuration}秒)");
  569. return;
  570. } elseif (in_array($taskStatus, ['pending', 'processing', 'queued'])) {
  571. // 任务处理中,继续轮询
  572. $this->comment(" 任务处理中,继续等待...");
  573. continue;
  574. } else {
  575. // 未知状态
  576. $this->warn(" 未知的任务状态: {$taskStatus}");
  577. $this->info(" 完整响应: " . json_encode($queryData, JSON_UNESCAPED_UNICODE));
  578. continue;
  579. }
  580. } catch (\Exception $e) {
  581. $this->warn(" 查询出错: " . $e->getMessage());
  582. continue;
  583. }
  584. }
  585. // 超时
  586. $totalEndTime = microtime(true);
  587. $totalDuration = round($totalEndTime - $startTime, 2);
  588. $this->error("\n✗ 查询超时,已轮询 {$maxAttempts} 次(约 " . ($maxAttempts * $interval / 60) . " 分钟)");
  589. $this->info("任务ID: {$taskId}");
  590. $this->info("总执行时间: {$totalDuration}秒");
  591. $this->info("您可以稍后手动查询任务状态");
  592. } catch (\GuzzleHttp\Exception\RequestException $e) {
  593. $endTime = microtime(true);
  594. $duration = round($endTime - $startTime, 2);
  595. $this->error("\nAPI请求失败(执行时间: {$duration}秒):");
  596. if ($e->hasResponse()) {
  597. $errorResponse = json_decode($e->getResponse()->getBody()->getContents(), true);
  598. $this->error(json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
  599. } else {
  600. $this->error($e->getMessage());
  601. }
  602. } catch (\Exception $e) {
  603. $endTime = microtime(true);
  604. $duration = round($endTime - $startTime, 2);
  605. $this->error("\n发生错误(执行时间: {$duration}秒): " . $e->getMessage());
  606. $this->error("错误堆栈: " . $e->getTraceAsString());
  607. }
  608. }
  609. /**
  610. * 处理历史视频任务 - 压缩视频并更新 compressed_url
  611. */
  612. private function handleHistoryTasks()
  613. {
  614. $this->info('开始处理历史视频任务...');
  615. try {
  616. // 查询所有 result_url 有值但 compressed_url 为空的成功任务
  617. $tasks = DB::table('mp_generate_video_tasks')
  618. ->where('status', 'success')
  619. ->whereNotNull('result_url')
  620. ->where('result_url', '!=', '')
  621. ->where(function($query) {
  622. $query->whereNull('compressed_url')
  623. ->orWhere('compressed_url', '');
  624. })
  625. ->select('id', 'result_url')
  626. ->get();
  627. $total = $tasks->count();
  628. $this->info("找到 {$total} 个需要处理的任务");
  629. if ($total === 0) {
  630. $this->info('没有需要处理的任务');
  631. return;
  632. }
  633. $successCount = 0;
  634. $failCount = 0;
  635. foreach ($tasks as $index => $task) {
  636. $taskId = $task->id;
  637. $resultUrl = $task->result_url;
  638. $current = $index + 1;
  639. $this->info("处理任务 [{$taskId}] ({$current}/{$total})");
  640. try {
  641. // 压缩视频
  642. $compressedUrl = compressVideo($resultUrl);
  643. // 如果压缩失败,使用原始URL
  644. $finalUrl = $compressedUrl ?: $resultUrl;
  645. // 更新数据库
  646. $updated = DB::table('mp_generate_video_tasks')
  647. ->where('id', $taskId)
  648. ->update([
  649. 'compressed_url' => $finalUrl,
  650. 'updated_at' => date('Y-m-d H:i:s')
  651. ]);
  652. if ($updated) {
  653. $successCount++;
  654. if ($compressedUrl) {
  655. $this->info("✓ 任务 [{$taskId}] 压缩成功");
  656. } else {
  657. $this->warn("⚠ 任务 [{$taskId}] 压缩失败,使用原始URL");
  658. }
  659. } else {
  660. $failCount++;
  661. $this->error("✗ 任务 [{$taskId}] 更新失败");
  662. }
  663. } catch (\Exception $e) {
  664. $failCount++;
  665. $this->error("✗ 任务 [{$taskId}] 处理异常: " . $e->getMessage());
  666. dLog('command')->error('处理历史视频任务异常', [
  667. 'task_id' => $taskId,
  668. 'error' => $e->getMessage()
  669. ]);
  670. }
  671. // 每处理10个任务休息一下,避免压力过大
  672. if ($current % 10 === 0) {
  673. sleep(1);
  674. }
  675. }
  676. $this->info("历史视频任务处理完成!");
  677. $this->info("总数: {$total}, 成功: {$successCount}, 失败: {$failCount}");
  678. } catch (\Exception $e) {
  679. $this->error('处理历史视频任务失败: ' . $e->getMessage());
  680. dLog('command')->error('处理历史视频任务失败', ['error' => $e->getMessage()]);
  681. }
  682. }
  683. /**
  684. * 处理历史分镜数据 - 根据视频任务更新分镜表
  685. */
  686. private function handleHistorySegments()
  687. {
  688. $this->info('开始处理历史分镜数据...');
  689. try {
  690. // 查询所有有 video_task_id 的分镜
  691. $segments = DB::table('mp_episode_segments')
  692. ->whereNotNull('video_task_id')
  693. ->where('video_task_id', '>', 0)
  694. ->select('segment_id', 'video_task_id')
  695. ->get();
  696. $total = $segments->count();
  697. $this->info("找到 {$total} 个需要处理的分镜");
  698. if ($total === 0) {
  699. $this->info('没有需要处理的分镜');
  700. return;
  701. }
  702. $successCount = 0;
  703. $skipCount = 0;
  704. $failCount = 0;
  705. foreach ($segments as $index => $segment) {
  706. $segmentId = $segment->segment_id;
  707. $videoTaskId = $segment->video_task_id;
  708. $current = $index + 1;
  709. $this->info("处理分镜 [{$segmentId}] ({$current}/{$total})");
  710. try {
  711. // 查询对应的视频任务
  712. $videoTask = DB::table('mp_generate_video_tasks')
  713. ->where('id', $videoTaskId)
  714. ->where('status', 'success')
  715. ->whereNotNull('result_url')
  716. ->where('result_url', '!=', '')
  717. ->whereNotNull('compressed_url')
  718. ->where('compressed_url', '!=', '')
  719. ->select('result_url', 'compressed_url')
  720. ->first();
  721. if (!$videoTask) {
  722. $skipCount++;
  723. $this->warn("⊘ 分镜 [{$segmentId}] 对应的视频任务不存在或未完成");
  724. continue;
  725. }
  726. // 更新分镜表
  727. $updated = DB::table('mp_episode_segments')
  728. ->where('segment_id', $segmentId)
  729. ->update([
  730. 'origin_video_url' => $videoTask->result_url,
  731. 'video_url' => $videoTask->compressed_url,
  732. 'updated_at' => date('Y-m-d H:i:s')
  733. ]);
  734. if ($updated) {
  735. $successCount++;
  736. $this->info("✓ 分镜 [{$segmentId}] 更新成功");
  737. } else {
  738. $failCount++;
  739. $this->error("✗ 分镜 [{$segmentId}] 更新失败");
  740. }
  741. } catch (\Exception $e) {
  742. $failCount++;
  743. $this->error("✗ 分镜 [{$segmentId}] 处理异常: " . $e->getMessage());
  744. dLog('command')->error('处理历史分镜数据异常', [
  745. 'segment_id' => $segmentId,
  746. 'video_task_id' => $videoTaskId,
  747. 'error' => $e->getMessage()
  748. ]);
  749. }
  750. // 每处理10个分镜休息一下
  751. if ($current % 10 === 0) {
  752. sleep(1);
  753. }
  754. }
  755. $this->info("历史分镜数据处理完成!");
  756. $this->info("总数: {$total}, 成功: {$successCount}, 跳过: {$skipCount}, 失败: {$failCount}");
  757. } catch (\Exception $e) {
  758. $this->error('处理历史分镜数据失败: ' . $e->getMessage());
  759. dLog('command')->error('处理历史分镜数据失败', ['error' => $e->getMessage()]);
  760. }
  761. }
  762. }