cb-demo / src /colab_utils.py
DimmingLight's picture
Upload folder using huggingface_hub
9e77d3a verified
Raw
History Blame Contribute Delete
7.54 kB
"""Helpers for running notebooks on Google Colab Pro+.
Each Colab notebook in ``notebooks/colab/`` calls :func:`setup_colab` in its
first cell to bootstrap the environment (mount Drive, install missing pip
deps, symlink data files into the cloned repo, configure git for auto-push).
After training cells complete and the user has eyeballed the output,
they manually run a final cell calling :func:`push_results` to commit
``results/*.json`` and push to ``main``.
The notebook ``.ipynb`` file itself is pushed via Colab's built-in
**File → Save a copy in GitHub** (uses Colab's own GitHub OAuth, no PAT
needed).
"""
from __future__ import annotations
import logging
import subprocess
import sys
from pathlib import Path
logger = logging.getLogger(__name__)
DRIVE_MOUNT = Path("/content/drive")
DEFAULT_DRIVE_ROOT = DRIVE_MOUNT / "MyDrive" / "thesis-cyberbullying"
DEFAULT_BRANCH = "main"
EXTRA_PIP_DEPS = ("Sastrawi==1.0.1", "sentencepiece", "gensim>=4.0.0")
def is_colab() -> bool:
"""True when running inside a Google Colab kernel."""
return "google.colab" in sys.modules
def _run(cmd: list[str], cwd: str | Path | None = None, capture: bool = False):
"""Run a shell command, raising on non-zero exit."""
return subprocess.run(
cmd, cwd=str(cwd) if cwd else None, check=True,
capture_output=capture, text=True,
)
def mount_drive() -> Path:
"""Mount Google Drive at ``/content/drive`` (idempotent). Return ``MyDrive`` path."""
if not is_colab():
return DRIVE_MOUNT / "MyDrive"
if not (DRIVE_MOUNT / "MyDrive").exists():
from google.colab import drive # noqa: WPS433
drive.mount(str(DRIVE_MOUNT))
return DRIVE_MOUNT / "MyDrive"
def install_deps(pkgs: tuple[str, ...] = EXTRA_PIP_DEPS) -> None:
"""``pip install -q`` packages Colab doesn't preinstall (Sastrawi, sentencepiece)."""
if not is_colab():
return
_run([sys.executable, "-m", "pip", "install", "-q", *pkgs])
def link_data_from_drive(
drive_root: Path = DEFAULT_DRIVE_ROOT,
repo_root: Path | None = None,
) -> list[str]:
"""Symlink ``drive_root/data/*`` into ``repo_root/data/raw/``.
Idempotent - existing symlinks/files are skipped.
"""
if repo_root is None:
repo_root = Path.cwd()
drive_data = drive_root / "data"
if not drive_data.exists():
raise FileNotFoundError(
f"Expected data files at {drive_data}. "
"Upload Dataset_Komentar_Tiktok.csv, cc.id.300.bin, "
"and slang JSON dicts there. See README.md § Colab workflow."
)
target = repo_root / "data" / "raw"
target.mkdir(parents=True, exist_ok=True)
linked: list[str] = []
for src in sorted(drive_data.iterdir()):
link = target / src.name
if link.exists() or link.is_symlink():
continue
link.symlink_to(src)
linked.append(src.name)
return linked
def configure_git(
github_pat: str,
repo_root: Path | None = None,
user_email: str = "colab@cyberbullying-detection",
user_name: str = "Colab Auto-Push",
) -> None:
"""Configure git for auto-push from Colab using a Personal Access Token."""
if repo_root is None:
repo_root = Path.cwd()
_run(["git", "config", "user.email", user_email], cwd=repo_root)
_run(["git", "config", "user.name", user_name], cwd=repo_root)
res = _run(["git", "remote", "get-url", "origin"], cwd=repo_root, capture=True)
url = res.stdout.strip()
if "@github.com" not in url:
new_url = url.replace("https://github.com/", f"https://{github_pat}@github.com/")
_run(["git", "remote", "set-url", "origin", new_url], cwd=repo_root)
def setup_colab(
drive_root: Path = DEFAULT_DRIVE_ROOT,
github_pat: str | None = None,
) -> dict:
"""One-shot bootstrap. Call this in the first cell of every Colab notebook.
Returns a paths dict with ``colab``, ``repo_root``, ``drive_root``,
``fasttext_path``, ``dataset_csv``, ``linked``.
"""
if not is_colab():
return {"colab": False, "repo_root": str(Path.cwd())}
mount_drive()
install_deps()
repo_root = Path.cwd()
linked = link_data_from_drive(drive_root, repo_root)
if github_pat:
configure_git(github_pat, repo_root)
fasttext_path = drive_root / "data" / "cc.id.300.bin"
dataset_csv = repo_root / "data" / "raw" / "Dataset_Komentar_Tiktok.csv"
# Model checkpoints persist to Drive so they survive Colab session resets
# and sync to the Windows host via Drive Desktop. App.py loads from the
# local repo `models/` - copy/symlink from `<drive_root>/models/` after sync.
models_drive_dir = drive_root / "models"
models_drive_dir.mkdir(parents=True, exist_ok=True)
return {
"colab": True,
"repo_root": str(repo_root),
"drive_root": str(drive_root),
"fasttext_path": str(fasttext_path),
"dataset_csv": str(dataset_csv),
"models_dir": str(models_drive_dir),
"linked": linked,
}
def resolve_models_dir(paths: dict | None = None) -> Path:
"""Return the directory where model checkpoints should be saved.
In Colab, returns ``<drive_root>/models/`` (from the ``setup_colab`` paths
dict) so checkpoints survive session resets and sync to the host via Drive
Desktop. Locally, returns the repo's ``models/`` directory.
"""
if paths and paths.get("colab") and paths.get("models_dir"):
d = Path(paths["models_dir"])
else:
from src.paths import MODELS_DIR
d = MODELS_DIR
d.mkdir(parents=True, exist_ok=True)
return d
def push_results(
commit_message: str,
paths: list[str] | None = None,
branch: str = DEFAULT_BRANCH,
repo_root: Path | None = None,
) -> bool:
"""Stage, commit, and push specified paths to ``branch``.
Order: stage → commit → fetch+rebase → push. The fetch+rebase happens AFTER
commit so the working tree is clean (notebook outputs that overwrote
tracked files would otherwise cause ``pull --rebase`` to refuse with
"you have unstaged changes" / fatal exit 128).
Returns True if a push happened, False if there was nothing to commit.
"""
if not is_colab():
print("Not in Colab - push_results is a no-op locally.")
return False
if repo_root is None:
repo_root = Path.cwd()
paths = paths or ["results/"]
# 1. Stage
_run(["git", "add", *paths], cwd=repo_root)
# 2. Commit (if anything to commit). Skip the rest only if there's truly
# nothing local - a no-op return is fine; nothing was changed.
diff = subprocess.run(
["git", "diff", "--cached", "--quiet"], cwd=str(repo_root),
)
if diff.returncode == 0:
print("No changes to commit.")
return False
_run(["git", "commit", "-m", commit_message], cwd=repo_root)
# 3. Fetch + rebase onto remote (handles parallel pushes from other notebooks).
# With a clean tree (we just committed), this won't fail on dirty-tree.
try:
_run(["git", "fetch", "origin", branch], cwd=repo_root)
_run(["git", "rebase", f"origin/{branch}"], cwd=repo_root)
except subprocess.CalledProcessError as exc:
print(
f"WARN: rebase onto origin/{branch} failed ({exc}). "
"Continuing - will rely on push to detect non-fast-forward."
)
# 4. Push
_run(["git", "push", "origin", branch], cwd=repo_root)
print(f"Pushed: {commit_message}")
return True