{
  "nbformat": 4,
  "nbformat_minor": 5,
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python"
    },
    "colab": {
      "name": "Stockfilm GFPGAN starter"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# Stockfilm: try GFPGAN on an image and a short video\n",
        "\n",
        "This notebook downloads Stockfilm's starter kit, creates a separate Python 3.11 environment, and runs the same **CPU** recipe as the local guide. Open it in Google Colab using **File \u2192 Upload notebook**, then run cells in order. A Google account and internet access are required. Your uploads are processed in Google's runtime; use the local kit when you want processing on your own machine.\n",
        "\n",
        "The core recipe was tested on Linux CPU. The Colab interface and GPU path have not been independently tested. No GPU is needed for the supplied short experiment. Full videos can take much longer than their playback length.\n",
        "\n",
        "GFPGAN generates plausible facial detail. Compare eyes, mouth, apparent age, hair boundaries, and motion before using a result. Keep your source. These are new tutorial experiments, separate from Stockfilm's historical wipes.\n"
      ],
      "id": "stockfilm-00"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 1. Download the starter kit\n",
        "\n",
        "The ZIP includes code and the small tutorial samples. It does not contain model weights. Setup downloads the recorded upstream weights and checks their SHA-256 hashes. The MIT code license and footage permission are separate; samples are supplied for following this tutorial. Production reuse needs ordinary Stockfilm licensing.\n"
      ],
      "id": "stockfilm-01"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "from pathlib import Path\n",
        "import hashlib, json, urllib.request, zipfile\n",
        "\n",
        "base_url = \"https://stockfilm.com/research/ai-restoration/assets/\"\n",
        "release = json.loads(urllib.request.urlopen(base_url + \"kit-release.json\").read())\n",
        "archive = Path(\"stockfilm-gfpgan-starter.zip\")\n",
        "urllib.request.urlretrieve(base_url + \"stockfilm-gfpgan-starter.zip\", archive)\n",
        "actual = hashlib.sha256(archive.read_bytes()).hexdigest()\n",
        "assert actual == release[\"zip_sha256\"], \"Starter ZIP checksum mismatch; stopped.\"\n",
        "kit = Path(\"stockfilm-gfpgan-starter\").resolve()\n",
        "with zipfile.ZipFile(archive) as bundle:\n",
        "    for item in bundle.infolist():\n",
        "        destination = (Path.cwd() / item.filename).resolve()\n",
        "        assert destination.is_relative_to(kit), \"Unexpected ZIP member; stopped.\"\n",
        "    bundle.extractall(Path.cwd())\n",
        "print((kit / \"SAMPLE-TERMS.txt\").read_text())\n"
      ],
      "id": "stockfilm-02"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 2. Install the pinned CPU environment\n",
        "\n",
        "This uses a separate Python 3.11.14 interpreter so changes to Colab's default Python do not silently change the recipe. Installation downloads roughly half a gigabyte of model assets plus Python packages. FFmpeg is normally present in Colab; setup stops with an instruction if it is missing.\n"
      ],
      "id": "stockfilm-03"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "import os, subprocess, sys\n",
        "\n",
        "subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"uv==0.8.19\"], check=True)\n",
        "subprocess.run([\"uv\", \"python\", \"install\", \"3.11.14\"], check=True)\n",
        "python311 = subprocess.check_output([\"uv\", \"python\", \"find\", \"3.11.14\"], text=True).strip()\n",
        "env = dict(os.environ, STOCKFILM_PYTHON=python311)\n",
        "subprocess.run([\"bash\", \"setup.sh\", \"cpu\"], cwd=kit, env=env, check=True)\n",
        "python = str(kit / \".venv/bin/python\")\n"
      ],
      "id": "stockfilm-04"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 3. Restore the supplied image\n",
        "\n",
        "The output keeps the image dimensions. Background enhancement is off. Detected faces are aligned, restored with GFPGAN v1.4, and pasted back using the upstream face mask. A run manifest records versions, hashes, timing, and detected face counts. The output directory must be new; change the run name if you repeat the cell.\n"
      ],
      "id": "stockfilm-05"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "subprocess.run([python, \"restore.py\", \"image\", \"sample-image.png\", \"--output\", \"runs/image-v14\"], cwd=kit, check=True)\n",
        "from IPython.display import display, Image\n",
        "display(Image(filename=str(kit / \"sample-image.png\")))\n",
        "display(Image(filename=str(kit / \"runs/image-v14/restored.png\")))\n",
        "image_manifest = json.loads((kit / \"runs/image-v14/manifest.json\").read_text())\n",
        "print(\"Faces:\", image_manifest[\"image\"][\"faces_detected\"], \"Elapsed seconds:\", image_manifest[\"elapsed_seconds\"])\n"
      ],
      "id": "stockfilm-06"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 4. Preflight and restore the 2-second sample video\n",
        "\n",
        "This starter supports progressive, square-pixel, normally oriented, 8-bit SDR video with constant frame rate and aligned zero-based audio/video starts. It checks decoded frame timestamps and rejects unsupported VFR, HDR, interlacing, rotation, or audio offsets. Each frame is restored separately; this is not a temporal model, and faces can flicker. The supplied sample is silent. Supported audio is explicitly transcoded to AAC; source metadata, subtitles, and data streams are not copied.\n"
      ],
      "id": "stockfilm-07"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "subprocess.run([python, \"restore.py\", \"preflight\", \"sample-video.mp4\"], cwd=kit, check=True)\n",
        "subprocess.run([python, \"restore.py\", \"video\", \"sample-video.mp4\", \"--output\", \"runs/video-v14\"], cwd=kit, check=True)\n",
        "from IPython.display import Video\n",
        "display(Video(str(kit / \"runs/video-v14/restored.mp4\"), embed=True))\n",
        "video_manifest = json.loads((kit / \"runs/video-v14/manifest.json\").read_text())\n",
        "print(json.dumps({key: video_manifest[\"video\"][\"output\"][key] for key in (\"frame_rate\", \"frame_count\", \"duration_seconds\")}, indent=2))\n"
      ],
      "id": "stockfilm-08"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 5. Try your own file\n",
        "\n",
        "Upload one small image or short supported video. You need permission to process the footage. This next cell restores an image; for a video replace `image` with `video`, and run `preflight` first. Choose a new output directory for every attempt.\n"
      ],
      "id": "stockfilm-09"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "# In Google Colab, uncomment these lines to upload one file:\n",
        "# from google.colab import files\n",
        "# uploaded = files.upload()\n",
        "# assert len(uploaded) == 1, \"Upload one file at a time.\"\n",
        "# own_file = Path(next(iter(uploaded))).resolve()\n",
        "# subprocess.run([python, \"restore.py\", \"image\", str(own_file), \"--output\", \"runs/my-image-01\"], cwd=kit, check=True)\n"
      ],
      "id": "stockfilm-10"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 6. Download your result and processing record\n",
        "\n",
        "The image/video files are viewing derivatives. The source files remain unchanged. Retain the run manifest with any result, and inspect your output against its input. Downloading the runs ZIP includes the staged video frames, which can be large for longer footage.\n"
      ],
      "id": "stockfilm-11"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "import shutil\n",
        "result_zip = shutil.make_archive(str(kit / \"tutorial-results\"), \"zip\", kit, \"runs\")\n",
        "# In Google Colab, uncomment to download:\n",
        "# from google.colab import files\n",
        "# files.download(result_zip)\n",
        "print(\"Results ZIP:\", result_zip)\n"
      ],
      "id": "stockfilm-12"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "See the kit's README for input limits, measured test results, and troubleshooting. Official sources: [GFPGAN](https://github.com/TencentARC/GFPGAN), [facexlib](https://github.com/xinntao/facexlib), [FFmpeg](https://ffmpeg.org/documentation.html). The site also links [Real-ESRGAN](https://github.com/xinntao/Real-ESRGAN) for a separate background enhancement experiment.\n"
      ],
      "id": "stockfilm-13"
    }
  ]
}
