{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": [],
      "gpuType": "T4"
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "accelerator": "GPU"
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# Studio 3D Commercial Pack — Cloud GPU Render Box (1-Clic)\n",
        "\n",
        "**Vous n'avez pas de carte graphique NVIDIA sur votre PC ou Mac ? Pas besoin d'installer Blender chez vous !**\n",
        "\n",
        "1. Dans le menu en haut de Google Colab, vérifiez que **Exécution > Modifier le type d'exécution** est bien sur **T4 GPU**.\n",
        "2. Cliquez simplement sur le bouton **️ Play** de la cellule ci-dessous.\n",
        "3. Copiez le lien `https://xxxx.trycloudflare.com` qui s'affiche et collez-le dans le fichier `.env` sur votre ordinateur (`BLENDER_ENDPOINT=https://xxxx.trycloudflare.com`).\n",
        "4. Votre Agent IA local (**Claude Code, Antigravity, Cursor, Codex**) va piloter ce Blender Cloud automatiquement, envoyer vos modèles 3D et rapatrier les images rendues sur votre PC !"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# ============================================================================\n",
        "# CELLULE UNIQUE 1-CLIC : INSTALLATION BLENDER GPU + SERVEUR + TUNNEL CLOUDFLARE\n",
        "# ============================================================================\n",
        "import os, sys, subprocess, tempfile, json, base64, io, zipfile, threading, time, re\n",
        "from http.server import BaseHTTPRequestHandler, HTTPServer\n",
        "from pathlib import Path\n",
        "\n",
        "WORKSPACE = Path('/content/studio3d_workspace')\n",
        "WORKSPACE.mkdir(parents=True, exist_ok=True)\n",
        "ACTIVE_BLEND = WORKSPACE / 'active_scene.blend'\n",
        "\n",
        "print('[1/3] Installation de Blender 4.2 LTS GPU (OptiX) sur le serveur Cloud...')\n",
        "if not Path('/opt/blender/blender').exists():\n",
        "    subprocess.run('apt-get update -qq && apt-get install -y -qq libxrender1 libxi6 libxkbcommon0 libsm6 libice6 libgl1-mesa-glx libegl1', shell=True, check=True)\n",
        "    subprocess.run('wget -q https://download.blender.org/release/Blender4.2/blender-4.2.3-linux-x64.tar.xz -O /tmp/blender.tar.xz', shell=True, check=True)\n",
        "    subprocess.run('mkdir -p /opt/blender && tar -xf /tmp/blender.tar.xz -C /opt/blender --strip-components=1', shell=True, check=True)\n",
        "    subprocess.run('ln -sf /opt/blender/blender /usr/local/bin/blender', shell=True, check=True)\n",
        "\n",
        "ver = subprocess.run(['blender', '--version'], capture_output=True, text=True).stdout.splitlines()[0]\n",
        "print(f'[OK] Blender installé : {ver}')\n",
        "\n",
        "print('[2/3] Installation du tunnel sécurisé Cloudflare (zéro compte requis)...')\n",
        "if not Path('/usr/local/bin/cloudflared').exists():\n",
        "    subprocess.run('wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -O /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared', shell=True, check=True)\n",
        "\n",
        "def run_blender_snippet(code_str, blend_file=None):\n",
        "    target_blend = Path(blend_file) if blend_file else ACTIVE_BLEND\n",
        "    wrapper = f'''\n",
        "import bpy, os, sys\n",
        "os.chdir({str(WORKSPACE)!r})\n",
        "if {str(WORKSPACE)!r} not in sys.path:\n",
        "    sys.path.insert(0, {str(WORKSPACE)!r})\n",
        "{code_str}\n",
        "bpy.ops.wm.save_as_mainfile(filepath={str(ACTIVE_BLEND)!r})\n",
        "'''\n",
        "    with tempfile.NamedTemporaryFile('w', suffix='.py', delete=False, encoding='utf-8') as f:\n",
        "        f.write(wrapper)\n",
        "        script_path = f.name\n",
        "    try:\n",
        "        cmd = ['blender', '-b']\n",
        "        if target_blend.is_file():\n",
        "            cmd.append(str(target_blend))\n",
        "        cmd.extend(['-P', script_path])\n",
        "        proc = subprocess.run(cmd, cwd=str(WORKSPACE), capture_output=True, text=True)\n",
        "        return proc.returncode, proc.stdout, proc.stderr\n",
        "    finally:\n",
        "        if os.path.exists(script_path):\n",
        "            os.remove(script_path)\n",
        "\n",
        "class Studio3DCloudHandler(BaseHTTPRequestHandler):\n",
        "    def log_message(self, fmt, *args):\n",
        "        pass\n",
        "\n",
        "    def do_GET(self):\n",
        "        body = json.dumps({'status': 'online', 'message': 'Studio 3D Cloud GPU Render Box connectee et prete !', 'blender': ver}).encode('utf-8')\n",
        "        self.send_response(200)\n",
        "        self.send_header('Content-Type', 'application/json')\n",
        "        self.send_header('Content-Length', str(len(body)))\n",
        "        self.end_headers()\n",
        "        self.wfile.write(body)\n",
        "\n",
        "    def do_POST(self):\n",
        "        length = int(self.headers.get('Content-Length', 0))\n",
        "        payload = json.loads(self.rfile.read(length).decode('utf-8'))\n",
        "        cmd_type = payload.get('type', 'execute_code')\n",
        "        params = payload.get('params') or {}\n",
        "\n",
        "        if cmd_type == 'upload_bundle':\n",
        "            b64_zip = params.get('zip_base64', '')\n",
        "            with zipfile.ZipFile(io.BytesIO(base64.b64decode(b64_zip)), 'r') as zf:\n",
        "                zf.extractall(WORKSPACE)\n",
        "                names = zf.namelist()\n",
        "            print(f'[OK] Assets synchronisés depuis le PC local ({len(names)} fichiers)')\n",
        "            res = {'status': 'success', 'extracted': len(names)}\n",
        "\n",
        "        elif cmd_type == 'execute_code':\n",
        "            code = params.get('code', '')\n",
        "            rc, out, err = run_blender_snippet(code, params.get('blend_file'))\n",
        "            print(f'[OK] Script Blender exécuté (code={rc})')\n",
        "            res = {'status': 'success' if rc == 0 else 'error', 'returncode': rc, 'stdout': out[-8000:], 'stderr': err[-4000:]}\n",
        "\n",
        "        elif cmd_type == 'render_still':\n",
        "            frame = int(params.get('frame', 1))\n",
        "            samples = int(params.get('samples', 48))\n",
        "            out_png = WORKSPACE / f'still_{frame:04d}.png'\n",
        "            snippet = f'''\n",
        "sc = bpy.context.scene\n",
        "sc.frame_set({frame})\n",
        "if sc.render.engine == 'CYCLES' and {samples} > 0:\n",
        "    sc.cycles.samples = {samples}\n",
        "sc.render.filepath = {str(out_png)!r}\n",
        "bpy.ops.render.render(write_still=True)\n",
        "'''\n",
        "            rc, out, err = run_blender_snippet(snippet, params.get('blend_file'))\n",
        "            b64_img = base64.b64encode(out_png.read_bytes()).decode('ascii') if out_png.is_file() else None\n",
        "            print(f'[OK] Still Frame {frame} rendue sur GPU Cloud ({samples} samples)')\n",
        "            res = {'status': 'success' if (rc == 0 and b64_img) else 'error', 'frame': frame, 'base64_png': b64_img, 'stdout': out[-3000:], 'stderr': err[-2000:]}\n",
        "\n",
        "        elif cmd_type == 'render_batch':\n",
        "            frames = params.get('frames') or [1]\n",
        "            samples = int(params.get('samples', 128))\n",
        "            batch_dir = WORKSPACE / 'batch_out'\n",
        "            batch_dir.mkdir(parents=True, exist_ok=True)\n",
        "            snippet = f'''\n",
        "sc = bpy.context.scene\n",
        "if sc.render.engine == 'CYCLES' and {samples} > 0:\n",
        "    sc.cycles.samples = {samples}\n",
        "sc.render.use_persistent_data = True\n",
        "for f in {frames!r}:\n",
        "    sc.frame_set(int(f))\n",
        "    sc.render.filepath = os.path.join({str(batch_dir)!r}, f'frame_{{int(f):04d}}.png')\n",
        "    bpy.ops.render.render(write_still=True)\n",
        "'''\n",
        "            rc, out, err = run_blender_snippet(snippet, params.get('blend_file'))\n",
        "            buf = io.BytesIO()\n",
        "            with zipfile.ZipFile(buf, 'w', zipfile.ZIP_STORED) as zf:\n",
        "                for f in frames:\n",
        "                    fp = batch_dir / f'frame_{int(f):04d}.png'\n",
        "                    if fp.is_file():\n",
        "                        zf.write(fp, fp.name)\n",
        "            print(f'[OK] Lot de frames {frames[0]}..{frames[-1]} rendu et envoyé au PC local')\n",
        "            res = {'status': 'success' if rc == 0 else 'error', 'zip_base64': base64.b64encode(buf.getvalue()).decode('ascii'), 'stdout': out[-3000:]}\n",
        "        else:\n",
        "            res = {'status': 'error', 'message': f'Unknown command: {cmd_type}'}\n",
        "\n",
        "        body = json.dumps(res).encode('utf-8')\n",
        "        self.send_response(200)\n",
        "        self.send_header('Content-Type', 'application/json')\n",
        "        self.send_header('Content-Length', str(len(body)))\n",
        "        self.end_headers()\n",
        "        self.wfile.write(body)\n",
        "\n",
        "server = HTTPServer(('0.0.0.0', 9876), Studio3DCloudHandler)\n",
        "threading.Thread(target=server.serve_forever, daemon=True).start()\n",
        "\n",
        "print('[3/3] Ouverture du tunnel public Cloudflare...')\n",
        "cf_proc = subprocess.Popen(['cloudflared', 'tunnel', '--url', 'http://localhost:9876'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n",
        "public_url = None\n",
        "for _ in range(60):\n",
        "    line = cf_proc.stdout.readline()\n",
        "    m = re.search(r'(https://[a-zA-Z0-9-]+\\.trycloudflare\\.com)', line)\n",
        "    if m:\n",
        "        public_url = m.group(1)\n",
        "        break\n",
        "\n",
        "print('\\n' + '='*76)\n",
        "print('STUDIO 3D CLOUD GPU RENDER BOX EST EN LIGNE !')\n",
        "print(f'COPIEZ CE LIEN DANS VOTRE .env : BLENDER_ENDPOINT={public_url}')\n",
        "print(f'Ou lancez sur votre PC : python tools/cloud_setup.py set-endpoint {public_url}')\n",
        "print('='*76 + '\\n')\n",
        "\n",
        "try:\n",
        "    while True:\n",
        "        time.sleep(10)\n",
        "except KeyboardInterrupt:\n",
        "    print('Arrêt du serveur Cloud.')"
      ]
    }
  ]
}
