{
 "nbformat": 4,
 "nbformat_minor": 0,
 "metadata": {
  "colab": {
   "provenance": [],
   "gpuType": "A100"
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3"
  },
  "accelerator": "GPU"
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# AI 新視界 Colab 實測 1：用 Google AI Pro 送的運算單元跑本站的 AI 主播（MiniMax H3 ref2va）\n",
    "做的事：裝 ComfyUI → 下載 H3 ref2va 的四個權重檔（約 43 GB，Comfy-Org 公開倉庫，不用登入）→ 用本站固定的主播圖＋一段台灣口音旁白，生成一段對嘴影片 → 記錄每一步時間、GPU 尖峰記憶體、運算單元。\n",
    "\n",
    "怎麼用：右上角先選 GPU（A100 優先，沒有就 L4），然後「執行階段 → 全部執行」。中途要做的只有：拿到 GPU 後，把「資源」面板上的「每小時使用 N 個運算單元」填到第 1 格的 UNITS_PER_HOUR，再重跑第 1 格（或一開始就填）。最後把第 8 格印出的整段文字貼回來。\n",
    "\n",
    "本站 A6000 的對照值：8 秒 480×864、20 步約 10 分鐘；GB10 約 15 分鐘。"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "#@title 1. 看看拿到什麼機器（先在右上角「執行階段 → 變更執行階段類型」選 GPU；建議 A100，拿不到就 L4）\n",
    "import subprocess, time, json, os, shutil, re\n",
    "T0 = time.time()\n",
    "UNITS_PER_HOUR = 0  #@param {type:\"number\"}\n",
    "VARIANTS = \"pruned_int8\"  #@param [\"pruned_int8\", \"int8\", \"bf16\", \"pruned_int8,int8,bf16\", \"int8,bf16\"]\n",
    "#@markdown ↑ 要跑哪些版本（逗號分隔會依序跑）：pruned_int8＝本站地端用的修剪版；int8＝未修剪；bf16＝完整版（66 GB 權重＋51 GB 文字編碼器，要 A100 80 GB）。\n",
    "#@markdown ↑ 把右上角「資源」面板顯示的「每小時使用 N 個運算單元」填進來（拿到 GPU 後才會顯示），用來換算每支影片的成本；不填就只記時間。\n",
    "gpu = subprocess.run([\"nvidia-smi\",\"--query-gpu=name,memory.total,driver_version\",\"--format=csv,noheader\"],capture_output=True,text=True).stdout.strip()\n",
    "ram = int(next(l for l in open(\"/proc/meminfo\") if l.startswith(\"MemTotal\")).split()[1])//2**20\n",
    "disk = shutil.disk_usage(\"/content\").free//2**30\n",
    "import torch; print(\"GPU:\", gpu); print(f\"RAM {ram} GB, 可用磁碟 {disk} GB, torch {torch.__version__}, cuda {torch.version.cuda}\")\n",
    "RESULT = {\"gpu\": gpu, \"ram_gb\": ram, \"disk_free_gb\": disk, \"torch\": torch.__version__, \"units_per_hour\": UNITS_PER_HOUR, \"t\": {}}\n",
    "def mark(k): RESULT[\"t\"][k] = round(time.time()-T0); print(f\"[{RESULT['t'][k]:>5}s] {k}\")\n",
    "mark(\"probe\")"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "#@title 2. 掛 Google Drive（結果影片與紀錄存到 MyDrive/aiwv-colab/，跑完才不會隨機器消失）\n",
    "from google.colab import drive\n",
    "drive.mount(\"/content/drive\")\n",
    "OUT = \"/content/drive/MyDrive/aiwv-colab\"; os.makedirs(OUT, exist_ok=True)\n",
    "mark(\"drive\")"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "#@title 3. 安裝 ComfyUI（和本站 A6000 上的同一套工具鏈）\n",
    "%cd /content\n",
    "if not os.path.exists(\"/content/ComfyUI\"):\n",
    "    !git clone -q --depth 1 https://github.com/comfyanonymous/ComfyUI.git\n",
    "%cd /content/ComfyUI\n",
    "!pip install -q -r requirements.txt 2>&1 | tail -2\n",
    "!pip install -q hf_transfer huggingface_hub 2>&1 | tail -1\n",
    "mark(\"install_comfyui\")"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "#@title 4. 下載 H3 權重（Comfy-Org/MiniMax-H3；修剪版 43 GB、未修剪 67 GB、完整版 123 GB；記下載速度）\n",
    "os.environ[\"HF_HUB_ENABLE_HF_TRANSFER\"] = \"1\"\n",
    "from huggingface_hub import hf_hub_download\n",
    "# 每個版本配對的 ref2va 權重與文字編碼器（修剪版配 nvfp4，和本站地端完全相同；未修剪配 int8；完整版配 bf16）\n",
    "VAR = {\"pruned_int8\": (\"minimax_h3_ref2va_pruned_int8_convrot.safetensors\", \"qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors\"),\n",
    "       \"int8\":        (\"minimax_h3_ref2va_int8_convrot.safetensors\",        \"qwen3vl_32b_minimax_h3_int8_convrot.safetensors\"),\n",
    "       \"bf16\":        (\"minimax_h3_ref2va_bf16.safetensors\",                \"qwen3vl_32b_minimax_h3_bf16.safetensors\")}\n",
    "RUN = [v.strip() for v in VARIANTS.split(\",\") if v.strip() in VAR]\n",
    "FILES = [(\"vae/minimax_h3_video_vae_fp16.safetensors\", \"vae\"), (\"vae/minimax_h3_audio_vae_fp32.safetensors\", \"vae\")]\n",
    "for v in RUN:\n",
    "    FILES += [(f\"diffusion_models/{VAR[v][0]}\", \"diffusion_models\"), (f\"text_encoders/{VAR[v][1]}\", \"text_encoders\")]\n",
    "FILES = list(dict.fromkeys(FILES))\n",
    "print(\"要跑的版本：\", RUN)\n",
    "t = time.time(); total = 0\n",
    "for f, sub in FILES:\n",
    "    p = hf_hub_download(\"Comfy-Org/MiniMax-H3\", f, local_dir=\"/content/ComfyUI/models\")\n",
    "    total += os.path.getsize(p); print(f\"  {f}  {os.path.getsize(p)/2**30:.1f} GB\")\n",
    "dl = time.time()-t\n",
    "RESULT[\"download_gb\"] = round(total/2**30, 1); RESULT[\"download_mbps\"] = round(total/2**20/dl)\n",
    "print(f\"下載 {RESULT['download_gb']} GB 用 {dl:.0f} 秒 ≈ {RESULT['download_mbps']} MB/s\"); mark(\"download\")"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "#@title 5. 啟動 ComfyUI（背景）並等它就緒\n",
    "import urllib.request\n",
    "log = open(\"/content/comfyui.log\",\"w\")\n",
    "P = subprocess.Popen([\"python\",\"main.py\",\"--listen\",\"127.0.0.1\",\"--port\",\"8188\",\"--disable-auto-launch\"], cwd=\"/content/ComfyUI\", stdout=log, stderr=subprocess.STDOUT)\n",
    "for _ in range(120):\n",
    "    time.sleep(2)\n",
    "    try:\n",
    "        urllib.request.urlopen(\"http://127.0.0.1:8188/system_stats\", timeout=2).read(); break\n",
    "    except Exception: pass\n",
    "else:\n",
    "    print(open(\"/content/comfyui.log\").read()[-3000:]); raise SystemExit(\"ComfyUI 沒起來\")\n",
    "print(\"ComfyUI 就緒\"); mark(\"comfyui_ready\")\n",
    "\n",
    "import threading\n",
    "PEAK = {\"mb\": 0, \"stop\": False}\n",
    "def _watch():\n",
    "    while not PEAK[\"stop\"]:\n",
    "        try:\n",
    "            mb = int(subprocess.run([\"nvidia-smi\",\"--query-gpu=memory.used\",\"--format=csv,noheader,nounits\"],capture_output=True,text=True).stdout.split()[0]); PEAK[\"mb\"] = max(PEAK[\"mb\"], mb)\n",
    "        except Exception: pass\n",
    "        time.sleep(3)\n",
    "threading.Thread(target=_watch, daemon=True).start()\n",
    "\n",
    "def submit(graph):\n",
    "    req = urllib.request.Request(\"http://127.0.0.1:8188/prompt\", data=json.dumps({\"prompt\": graph}).encode(), headers={\"Content-Type\": \"application/json\"})\n",
    "    with urllib.request.urlopen(req) as r: return json.load(r)[\"prompt_id\"]\n",
    "def wait(pid, every=5):\n",
    "    t0 = time.time()\n",
    "    while True:\n",
    "        with urllib.request.urlopen(f\"http://127.0.0.1:8188/history/{pid}\") as r: h = json.load(r)\n",
    "        if pid in h:\n",
    "            st = h[pid].get(\"status\", {}); outs = []\n",
    "            for n in h[pid].get(\"outputs\", {}).values():\n",
    "                for k in (\"images\",\"video\",\"audio\",\"gifs\"):\n",
    "                    for f in n.get(k, []): outs.append(f\"{f.get('subfolder','')}/{f['filename']}\".lstrip(\"/\"))\n",
    "            return st.get(\"status_str\"), round(time.time()-t0), outs, st.get(\"messages\", [])\n",
    "        time.sleep(every)"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "#@title 6. 抓本站的主播圖與旁白（公開測試檔），算影格數\n",
    "for name in (\"persona-f.png\", \"narration-10s.wav\"):\n",
    "    urllib.request.urlretrieve(f\"https://aiview.tw/media/lab/colab/{name}\", f\"/content/ComfyUI/input/{name}\")\n",
    "import wave\n",
    "with wave.open(\"/content/ComfyUI/input/narration-10s.wav\") as w: dur = w.getnframes()/w.getframerate()\n",
    "PAD, W, H, STEPS, SEED = 0.6, 480, 864, 20, 1\n",
    "n = max(5, round((dur + PAD) * 24))\n",
    "while n % 17 != 5: n += 1\n",
    "print(f\"旁白 {dur:.2f} 秒 → {n} 影格（{n/24:.2f} 秒）{W}x{H} {STEPS} 步\")\n",
    "RESULT.update({\"audio_s\": round(dur,2), \"frames\": n, \"size\": f\"{W}x{H}\", \"steps\": STEPS})"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "#@title 7. 生成（和本站 host.py 同一張圖、同一個提示、同樣 20 步與種子）；每個版本跑一次，跑完先卸載模型\n",
    "PROMPT = (\"Use <Picture 1> as the presenter and <Audio 1> exactly as it is, as the presenter's own voice. \"\n",
    "          \"A calm, friendly Taiwanese news presenter in a quiet studio speaks directly to the camera, delivering the lines in <Audio 1> \"\n",
    "          \"with lips precisely in sync with the speech, natural subtle head movements and blinks, relaxed shoulders, hands out of frame. \"\n",
    "          \"Static camera, medium close-up, vertical framing, soft even broadcast lighting, plain deep slate-blue studio background. \"\n",
    "          \"No cuts, no camera movement, no background music, no sound effects, no on-screen text, no subtitles.\")\n",
    "def free():\n",
    "    try: urllib.request.urlopen(urllib.request.Request(\"http://127.0.0.1:8188/free\", data=json.dumps({\"unload_models\": True, \"free_memory\": True}).encode(), headers={\"Content-Type\": \"application/json\"})).read()\n",
    "    except Exception as e: print(\"free:\", e)\n",
    "RESULT[\"variants\"] = {}\n",
    "for v in RUN:\n",
    "  free(); time.sleep(5); PEAK[\"mb\"] = 0\n",
    "  g = {\n",
    "   \"1\": {\"class_type\": \"UNETLoader\", \"inputs\": {\"unet_name\": VAR[v][0], \"weight_dtype\": \"default\"}},\n",
    "   \"2\": {\"class_type\": \"CLIPLoader\", \"inputs\": {\"clip_name\": VAR[v][1], \"type\": \"minimax\", \"device\": \"default\"}},\n",
    "   \"3\": {\"class_type\": \"VAELoader\", \"inputs\": {\"vae_name\": \"minimax_h3_video_vae_fp16.safetensors\"}},\n",
    "   \"4\": {\"class_type\": \"VAELoader\", \"inputs\": {\"vae_name\": \"minimax_h3_audio_vae_fp32.safetensors\"}},\n",
    "   \"5\": {\"class_type\": \"LoadImage\", \"inputs\": {\"image\": \"persona-f.png\"}},\n",
    "   \"6\": {\"class_type\": \"LoadAudio\", \"inputs\": {\"audio\": \"narration-10s.wav\"}},\n",
    "   \"7\": {\"class_type\": \"MiniMaxH3ReferenceToVideo\", \"inputs\": {\"clip\": [\"2\", 0], \"vae\": [\"3\", 0], \"audio_vae\": [\"4\", 0], \"prompt\": PROMPT,\n",
    "        \"width\": W, \"height\": H, \"length\": n, \"ref_image_size\": \"match\", \"ref_images.ref_image_0\": [\"5\", 0], \"ref_audios.ref_audio_0\": [\"6\", 0]}},\n",
    "   \"8\": {\"class_type\": \"BasicGuider\", \"inputs\": {\"model\": [\"1\", 0], \"conditioning\": [\"7\", 0]}},\n",
    "   \"9\": {\"class_type\": \"KSamplerSelect\", \"inputs\": {\"sampler_name\": \"res_multistep\"}},\n",
    "   \"10\": {\"class_type\": \"BasicScheduler\", \"inputs\": {\"model\": [\"1\", 0], \"scheduler\": \"simple\", \"steps\": STEPS, \"denoise\": 1.0}},\n",
    "   \"11\": {\"class_type\": \"RandomNoise\", \"inputs\": {\"noise_seed\": SEED}},\n",
    "   \"12\": {\"class_type\": \"SamplerCustomAdvanced\", \"inputs\": {\"noise\": [\"11\", 0], \"guider\": [\"8\", 0], \"sampler\": [\"9\", 0], \"sigmas\": [\"10\", 0], \"latent_image\": [\"7\", 1]}},\n",
    "   \"13\": {\"class_type\": \"VAEDecode\", \"inputs\": {\"samples\": [\"12\", 0], \"vae\": [\"3\", 0]}},\n",
    "   \"14\": {\"class_type\": \"VAEDecodeAudio\", \"inputs\": {\"samples\": [\"12\", 0], \"vae\": [\"4\", 0]}},\n",
    "   \"15\": {\"class_type\": \"CreateVideo\", \"inputs\": {\"images\": [\"13\", 0], \"fps\": 24, \"audio\": [\"14\", 0]}},\n",
    "   \"16\": {\"class_type\": \"SaveVideo\", \"inputs\": {\"video\": [\"15\", 0], \"filename_prefix\": \"aiwv-h3-colab\", \"format\": \"auto\", \"codec\": \"auto\"}},\n",
    "  }\n",
    "  g[\"16\"][\"inputs\"][\"filename_prefix\"] = f\"aiwv-h3-{v}\"\n",
    "  print(f\"==== {v} ====\")\n",
    "  st, secs, outs, msgs = wait(submit(g))\n",
    "  print(st, f\"{secs} 秒\", outs, f\"尖峰 VRAM {PEAK['mb']} MB\")\n",
    "  r = {\"status\": st, \"generate_s\": secs, \"s_per_step\": round(secs/STEPS, 1), \"peak_vram_mb\": PEAK[\"mb\"]}\n",
    "  if st != \"success\":\n",
    "      for m in msgs:\n",
    "          if m[0] == \"execution_error\": r[\"error\"] = json.dumps(m[1], ensure_ascii=False)[:1500]; print(r[\"error\"])\n",
    "      print(open(\"/content/comfyui.log\").read()[-2000:])\n",
    "  else:\n",
    "      src = f\"/content/ComfyUI/output/{outs[0]}\"; shutil.copy(src, f\"{OUT}/h3-{v}-{gpu.split(',')[0].replace(' ','_')}.mp4\")\n",
    "      from IPython.display import Video, display; display(Video(src, embed=True, width=320))\n",
    "  RESULT[\"variants\"][v] = r\n",
    "  if UNITS_PER_HOUR: r[\"units_per_clip\"] = round(UNITS_PER_HOUR * secs / 3600, 2); r[\"twd_per_clip\"] = round(r[\"units_per_clip\"] * 0.1049 * 32, 1)\n",
    "  mark(f\"generate_{v}_done\")"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "#@title 8. 結果整理（把這一格印出來的整段貼回給站長的 AI 助理）\n",
    "PEAK[\"stop\"] = True\n",
    "RESULT[\"peak_vram_mb\"] = PEAK[\"mb\"]; RESULT[\"total_s\"] = round(time.time()-T0)\n",
    "if UNITS_PER_HOUR:\n",
    "    RESULT[\"units_per_session\"] = round(UNITS_PER_HOUR * RESULT[\"total_s\"] / 3600, 2)   # Pay-as-you-go 100 單元 US$10.49；1 美元約 32 元\n",
    "json.dump(RESULT, open(f\"{OUT}/h3-result.json\",\"w\"), ensure_ascii=False, indent=1)\n",
    "print(\"=== h3 RESULT ===\")\n",
    "print(json.dumps(RESULT, ensure_ascii=False, indent=1))\n",
    "print(\"=== END ===\")"
   ],
   "execution_count": null,
   "outputs": []
  }
 ]
}