Przeglądaj źródła

增加监测和修复历史视频任务中非720p的记录

lh 1 tydzień temu
rodzic
commit
22983d08eb
1 zmienionych plików z 281 dodań i 0 usunięć
  1. 281 0
      app/Console/Commands/FixNewVideoTasks720pCommand.php

+ 281 - 0
app/Console/Commands/FixNewVideoTasks720pCommand.php

@@ -0,0 +1,281 @@
+<?php
+
+namespace App\Console\Commands;
+
+use GuzzleHttp\Client;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
+
+/**
+ * 处理 id >= 12835 的新数据视频任务(动漫 act/分镜链路):
+ * 1. 480p 档任务切换为 720p(处理 480p -> 720p 档位残留);
+ * 2. 检查 sr_720p / 720p / 480p 成功任务的 result_url、compressed_url 是否真的是 720p:
+ *    - result 为 480p 档 -> 本地超分到 720p,重新压缩,更新任务两列 URL;
+ *    - result 已是 720p+ 但 compressed 为 480p 档 -> 基于 result 重新压缩;
+ *    - 同步关联 mp_episode_segments 的 origin_video_url / video_url(仅 URL,其余字段不动)。
+ *
+ * 注意:本命令依赖 ffmpeg/ffprobe 且需在 APP_ENV != local 的环境执行
+ * (upgrade480pTo720p / compressVideo 在 local 会短路)。
+ */
+class FixNewVideoTasks720pCommand extends Command
+{
+    /**
+     * @var string
+     */
+    protected $signature = 'video:fix-new-tasks-720p
+                            {--min-id=12835 : 只处理 id 大于等于该值的任务}
+                            {--apply : 真正执行超分与更新;不带此参数时仅探测并输出报告}';
+
+    /**
+     * @var string
+     */
+    protected $description = '检查并修复 id>=12835 新数据任务的 720p 交付(超分 result/compressed 并同步分镜 URL)';
+
+    /**
+     * @return int
+     */
+    public function handle(): int
+    {
+        if (env('APP_ENV') === 'local') {
+            $this->error('本命令需要在 APP_ENV != local 且安装 ffmpeg/ffprobe 的环境执行(local 下超分/压缩 helper 会短路)。');
+            return 1;
+        }
+
+        $apply = (bool)$this->option('apply');
+        $minId = (int)$this->option('min-id');
+
+        $this->info($apply ? '开始执行修复(--apply)...' : '检查模式:仅探测与输出报告,加 --apply 才会超分并更新。');
+
+        $tasks = DB::table('mp_generate_video_tasks')
+            ->select([
+                'id', 'video_resolution', 'status', 'api_type',
+                'alias_segment_id', 'alias_act_id', 'result_url', 'compressed_url',
+            ])
+            ->where('id', '>=', $minId)
+            ->whereIn('video_resolution', ['sr_720p', '720p', '480p'])
+            ->where('status', 'success')
+            ->whereNotNull('result_url')
+            ->where('result_url', '<>', '')
+            ->orderBy('id')
+            ->get();
+
+        $this->line(sprintf('候选任务数:%d', $tasks->count()));
+
+        $alreadyOk = 0;
+        $needResultFix = 0;
+        $needCompressedFix = 0;
+        $probeFailed = 0;
+        $updated = 0;
+
+        foreach ($tasks as $task) {
+            $resultProbe = $this->probeUrl($task->result_url);
+            if (!$resultProbe['ok']) {
+                $probeFailed++;
+                $this->warn(sprintf('#%d 探测 result_url 失败:%s', $task->id, $task->result_url));
+                continue;
+            }
+
+            $resultShort = min($resultProbe['width'], $resultProbe['height']);
+            $resultIs480 = $resultShort > 0 && abs($resultShort - 480) <= 60;
+
+            $compressedShort = null;
+            if (!empty($task->compressed_url)) {
+                $compProbe = $this->probeUrl($task->compressed_url);
+                if ($compProbe['ok']) {
+                    $compressedShort = min($compProbe['width'], $compProbe['height']);
+                }
+            }
+            $compressedIs480 = $compressedShort !== null && abs($compressedShort - 480) <= 60;
+
+            $this->line(sprintf(
+                '#%d [%s] result=%dx%d compressed=%s%s',
+                $task->id,
+                $task->video_resolution,
+                $resultProbe['width'],
+                $resultProbe['height'],
+                $compressedShort === null ? '?' : $compressedShort,
+                $compressedShort === null ? ' (探测失败)' : ''
+            ));
+
+            $needChange = false;
+            $newResult = $task->result_url;
+            $newCompressed = $task->compressed_url;
+
+            if ($resultIs480) {
+                $needResultFix++;
+                $needChange = true;
+                if ($apply) {
+                    $enhanced = upgrade480pTo720p($task->result_url, 'video', false, '720p');
+                    if ($enhanced && $enhanced !== $task->result_url) {
+                        $newResult = $enhanced;
+                        $compressed = compressVideo($enhanced);
+                        $newCompressed = $compressed ?: $enhanced;
+                        $this->line(sprintf(
+                            '  -> 超分完成:%s%s',
+                            $newResult,
+                            $compressed ? '' : '(重新压缩失败,compressed_url 使用超分结果)'
+                        ));
+                    } else {
+                        $this->warn(sprintf('#%d 超分失败,保留原 result_url', $task->id));
+                        $needChange = false;
+                    }
+                }
+            } elseif ($compressedIs480 && !empty($task->compressed_url)) {
+                $needCompressedFix++;
+                $needChange = true;
+                if ($apply) {
+                    $compressed = compressVideo($task->result_url);
+                    if ($compressed) {
+                        $newCompressed = $compressed;
+                        $this->line('  -> 基于已合格的 result 重新压缩完成');
+                    } else {
+                        $this->warn(sprintf('#%d 重新压缩失败,compressed_url 保持不变', $task->id));
+                        $needChange = false;
+                    }
+                }
+            } else {
+                $alreadyOk++;
+            }
+
+            if (!$needChange) {
+                continue;
+            }
+
+            if ($apply) {
+                DB::table('mp_generate_video_tasks')
+                    ->where('id', $task->id)
+                    ->update([
+                        'result_url' => $newResult,
+                        'compressed_url' => $newCompressed,
+                    ]);
+
+                $segmentSynced = $this->syncSegmentUrls($task, $newResult, $newCompressed);
+                $this->line(sprintf('#%d 已更新任务 URL(分镜同步:%s)', $task->id, $segmentSynced ? '是' : '跳过/无关联'));
+                $updated++;
+            }
+        }
+
+        $this->info(sprintf(
+            '汇总:待超分 result=%d,需重压 compressed=%d,已合格=%d,探测失败=%d,已更新任务=%d',
+            $needResultFix,
+            $needCompressedFix,
+            $alreadyOk,
+            $probeFailed,
+            $updated
+        ));
+
+        if (!$apply) {
+            $this->line('以上为检查结果;确认后在生产环境加 --apply 执行。');
+        }
+
+        return 0;
+    }
+
+    /**
+     * 下载并探测视频分辨率
+     *
+     * @param string $url
+     * @return array{ok:bool, width:int, height:int}
+     */
+    private function probeUrl(string $url): array
+    {
+        $tempDir = storage_path('app/temp/video_fix_check');
+        if (!is_dir($tempDir)) {
+            mkdir($tempDir, 0775, true);
+        }
+
+        $inputFile = '';
+        try {
+            $ext = getVideoExtFromUrl($url) ?: '.mp4';
+            $inputFile = $tempDir . '/' . uniqid('vfix_') . bin2hex(random_bytes(3)) . $ext;
+
+            $client = new Client(['timeout' => 300, 'verify' => false]);
+            $client->get($url, ['sink' => $inputFile]);
+
+            if (!file_exists($inputFile) || filesize($inputFile) <= 0) {
+                return ['ok' => false, 'width' => 0, 'height' => 0];
+            }
+
+            $ffprobe = env('FFPROBE_PATH', 'ffprobe');
+            $cmd = '"' . $ffprobe . '" -v quiet -print_format json -show_streams "' . $inputFile . '" 2>&1';
+            $output = shell_exec($cmd);
+            $info = json_decode((string)$output, true);
+
+            $width = 0;
+            $height = 0;
+            if (isset($info['streams']) && is_array($info['streams'])) {
+                foreach ($info['streams'] as $stream) {
+                    if (isset($stream['codec_type']) && $stream['codec_type'] === 'video') {
+                        $width = (int)($stream['width'] ?? 0);
+                        $height = (int)($stream['height'] ?? 0);
+                        break;
+                    }
+                }
+            }
+
+            return ['ok' => $width > 0 && $height > 0, 'width' => $width, 'height' => $height];
+        } catch (\Throwable $e) {
+            return ['ok' => false, 'width' => 0, 'height' => 0];
+        } finally {
+            if ($inputFile && file_exists($inputFile)) {
+                @unlink($inputFile);
+            }
+        }
+    }
+
+    /**
+     * 同步关联 mp_episode_segments 的 URL(仅 origin_video_url / video_url)
+     * 仅当分镜当前指向的就是该任务旧 URL 时才更新,避免覆盖同一分镜更新任务的产物。
+     *
+     * @param object $task
+     * @param string $newResultUrl
+     * @param string $newCompressedUrl
+     * @return bool
+     */
+    private function syncSegmentUrls($task, string $newResultUrl, string $newCompressedUrl): bool
+    {
+        if (!empty($task->alias_segment_id)) {
+            $segment = DB::table('mp_episode_segments')
+                ->where('segment_id', $task->alias_segment_id)
+                ->first(['id', 'origin_video_url', 'video_url']);
+            if (!$segment) {
+                return false;
+            }
+            $pointsToThisTask = $segment->origin_video_url === $task->result_url
+                || $segment->video_url === $task->compressed_url;
+            if (!$pointsToThisTask) {
+                return false;
+            }
+            DB::table('mp_episode_segments')
+                ->where('id', $segment->id)
+                ->update([
+                    'origin_video_url' => $newResultUrl,
+                    'video_url' => $newCompressedUrl,
+                ]);
+            return true;
+        }
+
+        if (!empty($task->alias_act_id)) {
+            $segment = DB::table('mp_episode_segments')
+                ->where('id', $task->alias_act_id)
+                ->first(['id', 'origin_video_url', 'video_url']);
+            if (!$segment) {
+                return false;
+            }
+            $pointsToThisTask = $segment->origin_video_url === $task->result_url
+                || $segment->video_url === $task->compressed_url;
+            if (!$pointsToThisTask) {
+                return false;
+            }
+            DB::table('mp_episode_segments')
+                ->where('id', $segment->id)
+                ->update([
+                    'origin_video_url' => $newResultUrl,
+                    'video_url' => $newCompressedUrl,
+                ]);
+            return true;
+        }
+
+        return false;
+    }
+}