From a04789e2fe9aadf27b456a2b906bd1c9cf8ec62e Mon Sep 17 00:00:00 2001 From: Jennifer Davis Date: Thu, 5 Nov 2026 01:48:23 +0000 Subject: [PATCH 1/3] docs(people-and-planet-ai): align wildlife notebook with updated cloud-sample-notebook standards Update people-and-planet-ai/image-classification/README.ipynb to adhere to the updated cloud-sample-notebook lifecycle and formatting guidelines while preserving the original Cell 0 ASF license header: - Use the 3-link launch badge bar (Colab, Colab Enterprise via /vertex-ai/colab/import/, and GitHub) and add the Author table. - Add Objectives, Costs (with Pricing Calculator link), and Prerequisites subsections under Overview. - Guard only !apt-get install libffi-dev behind google.colab and run the idempotent sparse-checkout unconditionally when not already inside people-and-planet-ai/image-classification. - Add # fmt: off / # fmt: on around @param configuration blocks. - Add a 'Validate target state' programmatic assertion section before 'Cleaning up' and default delete_resources to False. --- .../image-classification/README.ipynb | 100 ++++++++++++++---- 1 file changed, 80 insertions(+), 20 deletions(-) diff --git a/people-and-planet-ai/image-classification/README.ipynb b/people-and-planet-ai/image-classification/README.ipynb index 54c3c3958c..58e5983fb8 100644 --- a/people-and-planet-ai/image-classification/README.ipynb +++ b/people-and-planet-ai/image-classification/README.ipynb @@ -57,7 +57,7 @@ " \n", " \n", " \n", - " \n", + " \n", " \"Google
Open in Colab Enterprise\n", "
\n", " \n", @@ -67,7 +67,6 @@ " \n", " \n", "\n", - "\n", "\n", "
\n", "\n", @@ -81,7 +80,13 @@ "gcloud compute networks subnets create default --project=example-project-id --network=default --region=us-central1 --range=10.128.0.0/20 --enable-private-ip-google-access\n", "```\n", "\n", - "" + "\n", + "\n", + "---\n", + "\n", + "| Author |\n", + "| --- |\n", + "| [Google Cloud](https://github.com/GoogleCloudPlatform) |" ], "metadata": { "id": "1LUt29D0MSeu" @@ -115,7 +120,33 @@ " Use [`classify_wildlife_image.py`](classify_wildlife_image.py) to inspect camera trap photos, see how taxonomic family (`taxon_family`) resolves look-alike species, and pass `habitat_region` context to geo-fence predictions.\n", "4. **Generate multimodal embeddings for rare-species search**:\n", " Use [`embed_wildlife_image.py`](embed_wildlife_image.py) and **Gemini Embedding 2** (`gemini-embedding-2`) to turn a single reference photo of an endangered species into a 768-dimensional vector embedding for similarity search in BigQuery.\n", - "5. **Clean up resources** to avoid ongoing storage costs." + "5. **Clean up resources** to avoid ongoing storage costs.\n", + "\n", + "### Objectives\n", + "\n", + "In this tutorial, you learn how to:\n", + "\n", + "- Ingest and filter LILA WCS Camera Traps metadata into BigQuery using Apache Beam on Dataflow (`create_images_metadata_table.py`).\n", + "- Run class-balanced sampling (`Sample.FixedSizePerKey`) and batch multimodal inference (`RunInference` with `gemini-3.8-flash`) directly against `gs://public-datasets-lila/wcs-unzipped/` (`run_wildlife_inference.py`).\n", + "- Classify individual camera trap photos into structured ecological observations with taxonomic hierarchy (`taxon_family`) and habitat geo-fencing (`classify_wildlife_image.py`).\n", + "- Generate 768-dimensional multimodal vector embeddings with `gemini-embedding-2` and run BigQuery `VECTOR_SEARCH` for rare-species discovery (`embed_wildlife_image.py`).\n", + "\n", + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "- [Dataflow](https://cloud.google.com/dataflow/pricing)\n", + "- [BigQuery](https://cloud.google.com/bigquery/pricing)\n", + "- [Vertex AI / Gemini API](https://cloud.google.com/vertex-ai/generative-ai/pricing)\n", + "- [Cloud Storage](https://cloud.google.com/storage/pricing)\n", + "\n", + "Use the [Google Cloud Pricing Calculator](https://cloud.google.com/products/calculator) to generate a cost estimate based on your projected usage.\n", + "\n", + "### Prerequisites\n", + "\n", + "- A Google Cloud project with billing enabled.\n", + "- The Dataflow (`dataflow.googleapis.com`), Vertex AI (`aiplatform.googleapis.com`), and BigQuery (`bigquery.googleapis.com`) APIs enabled.\n", + "- IAM roles: Dataflow Admin (`roles/dataflow.admin`), BigQuery Admin (`roles/bigquery.admin`), Storage Admin (`roles/storage.admin`), and Vertex AI User (`roles/aiplatform.user`)." ], "metadata": { "id": "jXDRG8QK2BZD" @@ -154,19 +185,19 @@ " # Install libffi-dev for the Dataflow pipeline in ephemeral Colab runtimes.\n", " !apt-get -qq install libffi-dev\n", "\n", - " # Sparse-checkout only people-and-planet-ai/image-classification into an isolated directory.\n", - " REPO_DIR = \"ppai-image-classification\"\n", - " SAMPLE_SUBPATH = \"people-and-planet-ai/image-classification\"\n", - " BRANCH = \"main\"\n", + "# Sparse-checkout only people-and-planet-ai/image-classification into an isolated directory.\n", + "REPO_DIR = \"ppai-image-classification\"\n", + "SAMPLE_SUBPATH = \"people-and-planet-ai/image-classification\"\n", + "BRANCH = \"main\"\n", "\n", - " if not os.getcwd().endswith(SAMPLE_SUBPATH):\n", - " if not os.path.exists(REPO_DIR):\n", - " !git clone --filter=blob:none --no-checkout --depth 1 --sparse -b {BRANCH} https://github.com/GoogleCloudPlatform/python-docs-samples.git {REPO_DIR}\n", - " !git -C {REPO_DIR} sparse-checkout set {SAMPLE_SUBPATH}\n", - " %cd {REPO_DIR}\n", - " !git fetch --depth 1 origin {BRANCH}\n", - " !git -c advice.detachedHead=false checkout -f FETCH_HEAD\n", - " %cd {SAMPLE_SUBPATH}\n", + "if not os.getcwd().endswith(SAMPLE_SUBPATH):\n", + " if not os.path.exists(REPO_DIR):\n", + " !git clone --filter=blob:none --no-checkout --depth 1 --sparse -b {BRANCH} https://github.com/GoogleCloudPlatform/python-docs-samples.git {REPO_DIR}\n", + " !git -C {REPO_DIR} sparse-checkout set {SAMPLE_SUBPATH}\n", + " %cd {REPO_DIR}\n", + " !git fetch --depth 1 origin {BRANCH}\n", + " !git -c advice.detachedHead=false checkout -f FETCH_HEAD\n", + " %cd {SAMPLE_SUBPATH}\n", "\n", "if os.getcwd() not in sys.path:\n", " sys.path.insert(0, os.getcwd())\n", @@ -251,11 +282,13 @@ "cell_type": "code", "execution_count": null, "source": [ + "# fmt: off\n", "PROJECT_ID = \"example-project-id\" # @param {type: \"string\", placeholder: \"example-project-id\", isTemplate: true}\n", + "LOCATION = \"us-central1\" # @param {type: \"string\"}\n", + "# fmt: on\n", + "\n", "if not PROJECT_ID or PROJECT_ID == \"example-project-id\":\n", " PROJECT_ID = os.environ.get(\"GOOGLE_CLOUD_PROJECT\", \"example-project-id\")\n", - "\n", - "LOCATION = \"us-central1\" # @param {type: \"string\"}\n", "if not LOCATION:\n", " LOCATION = os.environ.get(\"GOOGLE_CLOUD_LOCATION\", \"us-central1\")\n", "\n", @@ -780,12 +813,36 @@ "execution_count": null, "outputs": [] }, + { + "cell_type": "markdown", + "source": [ + "### Validate target state\n", + "\n", + "Verify that all 6 gallery camera trap frames were embedded into 768-dimensional vectors and written to `embeddings.jsonl`:" + ], + "metadata": { + "id": "validate-target-state-md" + } + }, + { + "cell_type": "code", + "source": [ + "assert len(gallery) == 6, f\"Expected 6 gallery items, got {len(gallery)}\"\n", + "assert len(row[\"embedding\"]) == 768, f\"Expected 768-d embedding, got {len(row['embedding'])}\"\n", + "print(\"Target state validated: 6 camera trap embeddings (768 dimensions) verified.\")" + ], + "metadata": { + "id": "validate-target-state-cell" + }, + "execution_count": null, + "outputs": [] + }, { "cell_type": "markdown", "source": [ "## Cleaning up\n", "\n", - "To avoid incurring ongoing storage charges for the resources created in this notebook, run the following cell to delete the BigQuery dataset and the temporary Dataflow staging files in Cloud Storage:" + "To avoid incurring ongoing storage charges for the resources created in this notebook, set `delete_resources = True` and run the following cell to delete the BigQuery dataset and the temporary Dataflow staging files in Cloud Storage:" ], "metadata": { "id": "MGl_1UyiGVl6" @@ -794,7 +851,9 @@ { "cell_type": "code", "source": [ - "delete_resources = True # @param {type:\"boolean\"}\n", + "# fmt: off\n", + "delete_resources = False # @param {type:\"boolean\"}\n", + "# fmt: on\n", "\n", "if delete_resources or os.getenv(\"IS_TESTING\"):\n", " # Delete the BigQuery dataset (including the metadata, predictions, and embeddings tables).\n", @@ -804,6 +863,7 @@ " !gcloud storage rm -r \"gs://{BUCKET_NAME}/{CLOUD_STORAGE_DIRECTORY}\"" ], "metadata": { + "cellView": "form", "id": "8418262e" }, "execution_count": null, From 0aa581ce90ecfb2ef757e28622c132e089051e8f Mon Sep 17 00:00:00 2001 From: Jennifer Davis Date: Thu, 5 Nov 2026 06:29:49 +0000 Subject: [PATCH 2/3] docs(people-and-planet-ai): restore agent-platform Colab Enterprise badge URL in wildlife notebook Restore https://console.cloud.google.com/agent-platform/colab/import/... as the Open in Colab Enterprise badge link in people-and-planet-ai/image-classification/README.ipynb. --- people-and-planet-ai/image-classification/README.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/people-and-planet-ai/image-classification/README.ipynb b/people-and-planet-ai/image-classification/README.ipynb index 58e5983fb8..33dfba3b4c 100644 --- a/people-and-planet-ai/image-classification/README.ipynb +++ b/people-and-planet-ai/image-classification/README.ipynb @@ -57,7 +57,7 @@ " \n", " \n", " \n", - " \n", + " \n", " \"Google
Open in Colab Enterprise\n", "
\n", " \n", From 642db7982938d11629de9dbc5edb548791a3ae7b Mon Sep 17 00:00:00 2001 From: Jennifer Davis Date: Thu, 5 Nov 2026 23:56:32 +0000 Subject: [PATCH 3/3] fix(image-classification): validate embeddings.jsonl file contents in notebook and use patch.object - Update the Validate target state cell in people-and-planet-ai/image-classification/README.ipynb to read and parse embeddings.jsonl from disk and verify all 6 gallery rows (URI, species label, and 768-dimensional embedding vector) rather than only checking in-memory variables. - Replace raw string mock.patch("requests.get") with target-safe mock.patch.object in e2e_test.py. --- .../image-classification/README.ipynb | 20 ++++++++++++++++--- .../image-classification/e2e_test.py | 7 ++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/people-and-planet-ai/image-classification/README.ipynb b/people-and-planet-ai/image-classification/README.ipynb index 33dfba3b4c..2d3d4811bc 100644 --- a/people-and-planet-ai/image-classification/README.ipynb +++ b/people-and-planet-ai/image-classification/README.ipynb @@ -827,9 +827,23 @@ { "cell_type": "code", "source": [ - "assert len(gallery) == 6, f\"Expected 6 gallery items, got {len(gallery)}\"\n", - "assert len(row[\"embedding\"]) == 768, f\"Expected 768-d embedding, got {len(row['embedding'])}\"\n", - "print(\"Target state validated: 6 camera trap embeddings (768 dimensions) verified.\")" + "with open(\"embeddings.jsonl\") as f:\n", + " written_rows = [json.loads(line) for line in f if line.strip()]\n", + "\n", + "assert len(written_rows) == len(gallery), (\n", + " f\"Expected {len(gallery)} rows in embeddings.jsonl, got {len(written_rows)}\"\n", + ")\n", + "for i, record in enumerate(written_rows):\n", + " assert record[\"image_gcs_uri\"] == gallery[i][0], (\n", + " f\"Row {i} URI mismatch: {record['image_gcs_uri']} != {gallery[i][0]}\"\n", + " )\n", + " assert record[\"species_scientific_name\"] == gallery[i][1], (\n", + " f\"Row {i} species mismatch: {record['species_scientific_name']} != {gallery[i][1]}\"\n", + " )\n", + " assert len(record[\"embedding\"]) == 768, (\n", + " f\"Row {i} expected 768-d embedding, got {len(record['embedding'])}\"\n", + " )\n", + "print(\"Target state validated: 6 camera trap embeddings (768 dimensions) verified in embeddings.jsonl.\")" ], "metadata": { "id": "validate-target-state-cell" diff --git a/people-and-planet-ai/image-classification/e2e_test.py b/people-and-planet-ai/image-classification/e2e_test.py index b0b249c4f0..4e82c3ba41 100644 --- a/people-and-planet-ai/image-classification/e2e_test.py +++ b/people-and-planet-ai/image-classification/e2e_test.py @@ -117,7 +117,12 @@ def test_extract_metadata_fn_filters_invalid_categories() -> None: mock_response = mock.MagicMock() mock_response.content = zip_buffer.getvalue() - with mock.patch("requests.get", return_value=mock_response): + with mock.patch.object( + create_images_metadata_table.requests, + "get", + return_value=mock_response, + autospec=True, + ): fn = create_images_metadata_table._ExtractMetadataFn() results = list(fn.process(create_images_metadata_table.METADATA_URL))