轻音少女 S1
全流程监控
Sakura 翻译、SenseNova 多 Agent 审查与双裁判 MQM 的实时执行状态。页面每 4 秒自动读取持久化进度。
{"repo": "caiyilian/anime-accurate-sub", "files": {"scripts/season_monitor.py": {"4d507921cd926e659e5a5a22d58eec34686302b8": {"t": 1785768246.6065085, "n": 21253, "h": "c8d3c05e16668cd8", "c": "\"\"\"Read-only progress collector and dashboard for a full-season pipeline run.\"\"\"\n\n# The embedded, self-contained dashboard intentionally keeps CSS and JS compact.\n# ruff: noqa: E501\n\nfrom __future__ import annotations\n\nimport json\nimport re\nimport time\nfrom collections import Counter\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Any\n\nSEASON_STAGES = (\n (\"japanese_subtitle\", \"日文字幕\", 2),\n (\"translate\", \"Sakura 翻译\", 10),\n (\"multi_agent_review\", \"多 Agent 审查\", 38),\n (\"mqm_quality_review\", \"双裁判 MQM\", 42),\n (\"subtitle\", \"生成字幕\", 1),\n (\"embed_subtitle\", \"压制视频\", 6),\n (\"quality_check\", \"规则质检\", 1),\n)\nSTAGE_LABELS = dict((stage, label) for stage, label, _ in SEASON_STAGES)\nSTAGE_WEIGHTS = dict((stage, weight) for stage, _, weight in SEASON_STAGES)\nREVIEW_FILES = {\n \"multi_agent_review\": (\"multi_agent_review.progress.jsonl\", \"translated.json\"),\n \"mqm_quality_review\": (\"mqm_quality_review.progress.jsonl\", \"reviewed.json\"),\n}\nEPISODE_PATTERN = re.compile(r\"第\\s*(\\d{1,3})\\s*集\")\nANSI_PATTERN = re.compile(r\"\\x1b\\[[0-?]*[ -/]*[@-~]\")\nSECRET_PATTERNS = (\n re.compile(r\"(?i)(authorization\\s*[:=]\\s*bearer\\s+)[^\\s,;]+\"),\n re.compile(r\"(?i)((?:api[_-]?key|token|secret)\\s*[:=]\\s*)[^\\s,;]+\"),\n re.compile(r\"\\bsk-[A-Za-z0-9_-]{8,}\\b\"),\n)\n\n\ndef _iso_timestamp(timestamp: float | None) -> str | None:\n if timestamp is None:\n return None\n return datetime.fromtimestamp(timestamp).astimezone().isoformat(timespec=\"seconds\")\n\n\ndef _read_json(path: Path) -> Any:\n try:\n return json.loads(path.read_text(encoding=\"utf-8\"))\n except (OSError, UnicodeError, json.JSONDecodeError):\n return None\n\n\ndef _redact_log(text: str) -> str:\n redacted = ANSI_PATTERN.sub(\"\", text)\n for pattern in SECRET_PATTERNS:\n if pattern.groups:\n redacted = pattern.sub(r\"\\1[已隐藏]\", redacted)\n else:\n redacted = pattern.sub(\"[已隐藏]\", redacted)\n return redacted\n\n\nclass SeasonProgressMonitor:\n \"\"\"Summarize durable pipeline artifacts without controlling the process.\"\"\"\n\n def __init__(\n self,\n root: str | Path,\n episode_count: int = 14,\n stale_after_seconds: int = 300,\n log_line_count: int = 36,\n ) -> None:\n self.root = Path(root).resolve()\n self.episode_count = max(1, int(episode_count))\n self.stale_after_seconds = max(30, int(stale_after_seconds))\n self.log_line_count = max(5, int(log_line_count))\n self._jsonl_cache: dict[Path, tuple[tuple[int, int], dict[str, Any]]] = {}\n self._count_cache: dict[Path, tuple[tuple[int, int], int]] = {}\n\n @staticmethod\n def _signature(path: Path) -> tuple[int, int] | None:\n try:\n stat = path.stat()\n return stat.st_size, stat.st_mtime_ns\n except OSError:\n return None\n\n def _segment_count(self, path: Path) -> int:\n signature = self._signature(path)\n if signature is None:\n return 0\n cached = self._count_cache.get(path)\n if cached and cached[0] == signature:\n return cached[1]\n payload = _read_json(path)\n count = len(payload) if isinstance(payload, list) else 0\n self._count_cache[path] = (signature, count)\n return count\n\n def _review_progress(self, path: Path, total: int) -> dict[str, Any]:\n signature = self._signature(path)\n empty = {\"processed\": 0, \"total\": total, \"percent\": 0, \"statuses\": {}}\n if signature is None:\n return empty\n cached = self._jsonl_cache.get(path)\n if cached and cached[0] == signature:\n summary = dict(cached[1])\n summary[\"total\"] = total\n summary[\"percent\"] = round(100 * summary[\"processed\"] / total, 1) if total else 0\n return summary\n\n records: dict[int, dict[str, Any]] = {}\n try:\n with path.open(\"r\", encoding=\"utf-8\", errors=\"replace\") as source:\n for line in source:\n try:\n row = json.loads(line)\n except json.JSONDecodeError:\n continue\n position = row.get(\"position\") if isinstance(row, dict) else None\n if isinstance(position, int) and position >= 0:\n records[position] = row\n except OSError:\n return empty\n\n statuses = Counter(str(row.get(\"status\") or \"unknown\") for row in records.values())\n processed = len(records)\n summary = {\n \"processed\": processed,\n \"total\": total,\n \"percent\": round(100 * processed / total, 1) if total else 0,\n \"statuses\": dict(sorted(statuses.items())),\n }\n self._jsonl_cache[path] = (signature, dict(summary))\n return summary\n\n def _episode_directories(self) -> dict[int, Path]:\n result: dict[int, Path] = {}\n if not self.root.is_dir():\n return result\n try:\n children = self.root.iterdir()\n except OSError:\n return result\n for path in children:\n if not path.is_dir():\n continue\n match = EPISODE_PATTERN.search(path.name)\n if match:\n result[int(match.group(1))] = path\n return result\n\n @staticmethod\n def _newest_mtime(path: Path | None) -> float | None:\n if path is None or not path.is_dir():\n return None\n newest: float | None = None\n try:\n for child in path.iterdir():\n if child.is_file():\n modified = child.stat().st_mtime\n newest = modified if newest is None else max(newest, modified)\n except OSError:\n return newest\n return newest\n\n def _quality_summary(self, episode_dir: Path | None) -> dict[str, Any]:\n if episode_dir is None:\n return {}\n result: dict[str, Any] = {}\n mqm = _read_json(episode_dir / \"mqm_quality_report.json\")\n if isinstance(mqm, dict) and isinstance(mqm.get(\"summary\"), dict):\n result[\"mqm\"] = {\n key: mqm[\"summary\"].get(key, 0)\n for key in (\"approved\", \"corrected\", \"needs_review\", \"errors\", \"applied\")\n }\n final = _read_json(episode_dir / \"final_adjudication_summary.json\")\n if isinstance(final, dict) and isinstance(final.get(\"summary\"), dict):\n final_summary = final[\"summary\"]\n resolved = min(\n int(result[\"mqm\"][\"needs_review\"]),\n int(final_summary.get(\"resolved_needs_review\", 0)),\n )\n kept = min(resolved, int(final_summary.get(\"needs_review_kept\", 0)))\n revised = min(\n resolved - kept,\n int(final_summary.get(\"needs_review_revised\", 0)),\n )\n result[\"mqm\"][\"approved\"] += kept\n result[\"mqm\"][\"corrected\"] += revised\n result[\"mqm\"][\"needs_review\"] -= resolved\n result[\"mqm\"][\"final_reviewed\"] = int(\n final_summary.get(\"reviewed\", resolved)\n )\n quality = _read_json(episode_dir / \"quality_report.json\")\n if isinstance(quality, dict) and isinstance(quality.get(\"stats\"), dict):\n result[\"rules\"] = {\n key: quality[\"stats\"].get(key, 0)\n for key in (\"total_issues\", \"errors\", \"warnings\", \"info\")\n }\n return result\n\n def _episode_snapshot(self, number: int, episode_dir: Path | None) -> dict[str, Any]:\n checkpoint = _read_json(episode_dir / \"checkpoint.json\") if episode_dir else None\n checkpoint = checkpoint if isinstance(checkpoint, dict) else {}\n completed = {\n stage for stage, _, _ in SEASON_STAGES\n if isinstance(checkpoint.get(stage), dict)\n and checkpoint[stage].get(\"status\") == \"completed\"\n }\n failed = {\n stage for stage, _, _ in SEASON_STAGES\n if isinstance(checkpoint.get(stage), dict)\n and checkpoint[stage].get(\"status\") == \"failed\"\n }\n active = next((stage for stage, _, _ in SEASON_STAGES if stage not in completed), None)\n review = {\"processed\": 0, \"total\": 0, \"percent\": 0, \"statuses\": {}}\n if active in REVIEW_FILES and episode_dir:\n progress_name, source_name = REVIEW_FILES[active]\n total = self._segment_count(episode_dir / source_name)\n review = self._review_progress(episode_dir / progress_name, total)\n\n stages = []\n weighted_progress = 0.0\n for stage, label, weight in SEASON_STAGES:\n if stage in completed:\n state, fraction = \"completed\", 1.0\n elif stage in failed:\n state, fraction = \"failed\", 0.0\n elif stage == active:\n state = \"active\"\n fraction = review[\"percent\"] / 100 if stage in REVIEW_FILES else 0.0\n else:\n state, fraction = \"pending\", 0.0\n weighted_progress += weight * fraction\n stages.append({\"id\": stage, \"label\": label, \"state\": state})\n\n if len(completed) == len(SEASON_STAGES):\n state = \"completed\"\n elif failed:\n state = \"failed\"\n elif checkpoint or review[\"processed\"]:\n state = \"running\"\n else:\n state = \"pending\"\n updated = self._newest_mtime(episode_dir)\n return {\n \"number\": number,\n \"name\": episode_dir.name if episode_dir else f\"第{number:02d}集\",\n \"state\": state,\n \"percent\": round(weighted_progress, 1),\n \"active_stage\": active,\n \"active_stage_label\": STAGE_LABELS.get(active, \"已完成\"),\n \"completed_stages\": len(completed),\n \"total_stages\": len(SEASON_STAGES),\n \"stages\": stages,\n \"review\": review,\n \"quality\": self._quality_summary(episode_dir),\n \"updated_at\": _iso_timestamp(updated),\n \"_updated_epoch\": updated,\n }\n\n def _log_tail(self) -> tuple[str, float | None]:\n paths = [self.root / \"full_run_v3.stdout.log\", self.root / \"full_run.stdout.log\"]\n path = next((candidate for candidate in paths if candidate.is_file()), None)\n if path is None:\n return \"\", None\n try:\n size = path.stat().st_size\n with path.open(\"rb\") as source:\n source.seek(max(0, size - 65_536))\n lines = source.read().decode(\"utf-8\", errors=\"replace\").splitlines()\n return _redact_log(\"\\n\".join(lines[-self.log_line_count :])), path.stat().st_mtime\n except OSError:\n return \"\", None\n\n def snapshot(self) -> dict[str, Any]:\n directories = self._episode_directories()\n episodes = [\n self._episode_snapshot(number, directories.get(number))\n for number in range(1, self.episode_count + 1)\n ]\n log_tail, log_mtime = self._log_tail()\n activity_times = [\n timestamp for timestamp in [log_mtime, *(item[\"_updated_epoch\"] for item in episodes)]\n if timestamp is not None\n ]\n last_activity = max(activity_times) if activity_times else None\n now = time.time()\n completed_episodes = sum(item[\"state\"] == \"completed\" for item in episodes)\n failed = any(item[\"state\"] == \"failed\" for item in episodes)\n if completed_episodes == self.episode_count:\n status = \"completed\"\n elif failed:\n status = \"failed\"\n elif last_activity is not None and now - last_activity <= self.stale_after_seconds:\n status = \"running\"\n elif last_activity is not None:\n status = \"stale\"\n else:\n status = \"waiting\"\n\n active_episode = next((item for item in episodes if item[\"state\"] == \"running\"), None)\n if active_episode is None:\n active_episode = next((item for item in episodes if item[\"state\"] != \"completed\"), None)\n overall = sum(item[\"percent\"] for item in episodes) / self.episode_count\n for episode in episodes:\n episode.pop(\"_updated_epoch\", None)\n return {\n \"status\": status,\n \"status_label\": {\n \"running\": \"运行正常\",\n \"completed\": \"全季完成\",\n \"failed\": \"发现失败阶段\",\n \"stale\": \"等待新进度\",\n \"waiting\": \"尚未开始\",\n }[status],\n \"overall_percent\": round(overall, 1),\n \"completed_episodes\": completed_episodes,\n \"episode_count\": self.episode_count,\n \"active_episode\": active_episode[\"number\"] if active_episode else None,\n \"last_activity_at\": _iso_timestamp(last_activity),\n \"captured_at\": _iso_timestamp(now),\n \"root_name\": self.root.name,\n \"episodes\": episodes,\n \"log_tail\": log_tail,\n }\n\n\nMONITOR_HTML = \"\"\"\n
\nSakura 翻译、SenseNova 多 Agent 审查与双裁判 MQM 的实时执行状态。页面每 4 秒自动读取持久化进度。