{
  "id": "pct-2026-11-07-spotlight-7a74b68779",
  "content_type": "article",
  "desk": "spotlight",
  "title": "Cloud Run Jobs: a thumbnail factory with a one-minute meter",
  "publication_date": "2026-11-07",
  "observation_date": "2026-11-07",
  "editorial_voice": "AI-assisted Spotlight Desk",
  "body": "A video catalogue needs preview images. There are 1,000 short clips, each about a minute long, and the publishing team wants a small contact sheet's worth of frames from each. The work can run after the uploads finish. Keeping a web server available all day would solve a different problem.\n\nGoogle Cloud Run Jobs is a concrete option: package the extraction code in a container, divide the catalogue into tasks, run them, and exit. Google distinguishes jobs from request-serving Cloud Run services; a job can contain up to 10,000 independent tasks, with the application responsible for distributing the records. [S1]\n\nThe useful buying question is surprisingly small: should one clip equal one task? For this workload, the billing minimum makes that decision worth examining before choosing a larger machine.\n\n## Build a thumbnail factory\n\nPCT's proposed job uses FFmpeg on a CPU. For every five seconds of video, it produces one JPEG, resized to 320 pixels wide with proportional height. Google publishes its own Cloud Run video-transcoding tutorial using GPUs; this example deliberately asks a smaller question about CPU thumbnail extraction rather than assuming a GPU is necessary. [S6]\n\nStart with a fixed manifest of input objects, including each object's generation number. That identifies the specific stored version to process. The example accepts clips between five and sixty seconds and no larger than 50 MiB. Those are protective limits chosen for this recipe, not Cloud Run product limits.\n\nThe worker downloads one clip, runs the extraction, puts the images into one archive, uploads the completed archive, and moves to the next clip. One clip at a time keeps temporary files from accumulating. A representative command is:\n\n`ffmpeg -i input.mp4 -map 0:v:0 -vf \"fps=1/5,scale=320:-2\" -an frame-%04d.jpg`\n\nThe accompanying Python worker adds size checks, error handling and upload preconditions. A local smoke test produced twelve 320-by-180 JPEGs from a synthetic sixty-second video. That verifies a basic output path. The container has not been built or run in Google Cloud, and the smoke test establishes no cloud throughput or cost benchmark.\n\n## The minute that changes the arithmetic\n\nFor regular jobs in Iowa, us-central1, Google's price page lists $0.000018 per vCPU-second and $0.000002 per GiB-second. Jobs pay for an instance's lifetime, with a one-minute minimum for each started instance. The price snapshot here is November 7, 2026. It excludes delayed-job rates and commitment discounts. [S2]\n\nConsider a deliberately explicit planning case. Every task has one vCPU and one GiB of memory. Assume each clip takes twenty seconds for download, processing and upload, plus five seconds of container overhead per task. Assume no failures, retries, sidecars or overlapping executions. These times are modeling assumptions, not observed FFmpeg performance.\n\nThe combined compute rate is $0.000020 per instance-second. Give each clip its own task and its assumed twenty-five-second lifetime is billed as sixty seconds. Across 1,000 tasks, that is 60,000 billed seconds and **$1.20 of gross compute**.\n\nNow put twenty clips into each of fifty tasks. Each instance lives for an assumed 405 seconds: twenty times twenty, plus five. Total billed time becomes 20,250 seconds, costing **$0.405 of gross compute**. The model saves 79.5 cents, or about 66%, without changing the machine size or the amount of useful extraction work.\n\nThese are compute subtotals before free allowances, taxes, storage, object operations, image builds, registry charges, logging and applicable network charges. Google's free tier is shared across projects under a billing account, so an isolated demonstration may have no incremental Cloud Run compute charge at all. [S2] A small example should not be sold as a production invoice.\n\n## Make the bundle a recoverable unit\n\nGrouping records also enlarges the unit that can fail. If clip nineteen is corrupt, restarting an entire twenty-clip bundle wastes work unless the application remembers the eighteen completed results. Google's guidance recommends idempotent jobs and preserving output separately from input. [S3]\n\nOur recipe names each output using the input's bucket, object name, generation and recipe version. It uploads one completed archive per clip, then checks for that archive on a retry. A generation-match precondition of zero permits creation only when there is no live object at the destination name. [S5] That protects against competing attempts overwriting the same result.\n\nThe important distinction is between retrying a container and accepting its outputs. A permanent decoding failure still needs an error record and an operator decision. The minimal recipe fails the task; a production pipeline should quarantine the bad clip and reconcile the full manifest against accepted archives. Otherwise later clips in the same bundle can remain unattempted.\n\nCloud Run's normal writable filesystem is memory-backed and disappears when the instance stops. [S4] A fifty-megabyte source file is therefore part of the memory budget, alongside the decoder, Python process and output images. One GiB is a trial configuration, not a promise that every codec or high-resolution input will fit.\n\n## A small run before the large run\n\nThe supplied recipe includes a Dockerfile, a manifest generator and a worker. It assumes existing authorized buckets, an image repository and a service account with the required access.\n\nFor the 1,000-record case, fifty tasks produce twenty records per task. Set parallelism to ten to cap concurrent task instances. That setting controls how many bundles run together, not how many records belong in each bundle. Cloud Run exposes the task index and task count to the container so the worker can make the assignment. [S1]\n\nStart with a smaller representative manifest. Check image quality, peak memory, complete output coverage, retries and actual billed instance time. Then compare one-clip tasks with bundled tasks using the same inputs and acceptance checks. Long or difficult clips can make equal-sized bundles finish unevenly; a record count alone is a poor estimate of processing effort.\n\n## What the buyer actually learns\n\nThe saving in this example is less than a dollar per run. That is an argument for a simple implementation, not a new scheduling platform. At larger volume, the same minimum-charge effect can become material. At smaller volume, maintainability and correct outputs deserve more attention than the third decimal place on the CPU tariff.\n\nPCT's interpretation is that the unit of work belongs in a compute-price comparison. An attractive per-second price does not fully describe a service when every instance carries a minimum bill. Cloud Run Jobs earns a place on the shortlist when independent, containerized work can finish and release its resources. Whether it wins this catalogue depends on measured runtime, failure behavior and the full bill, not the illustrative saving above.",
  "sources": [
    {
      "id": "S1",
      "title": "Create jobs",
      "publisher": "Google Cloud",
      "published": null,
      "updated": "2026-10-30",
      "url": "https://docs.cloud.google.com/run/docs/create-jobs",
      "accessed": "2026-11-07",
      "access_note": "Opened primary documentation; checked task model, task count, task environment variables and creation controls."
    },
    {
      "id": "S2",
      "title": "Cloud Run pricing",
      "publisher": "Google Cloud",
      "published": null,
      "updated": null,
      "url": "https://cloud.google.com/run/pricing",
      "accessed": "2026-11-07",
      "access_note": "Opened current pricing page. Jobs table is set to Iowa (us-central1); checked default USD CPU/memory rates, account-wide free-tier aggregation, and minimum one-minute lifetime billing. Page has no displayed update date."
    },
    {
      "id": "S3",
      "title": "Jobs retries and checkpoints best practices",
      "publisher": "Google Cloud",
      "published": null,
      "updated": "2026-10-30",
      "url": "https://docs.cloud.google.com/run/docs/jobs-retries",
      "accessed": "2026-11-07",
      "access_note": "Opened primary documentation; checked restart, idempotence, separate outputs and checkpoint guidance."
    },
    {
      "id": "S4",
      "title": "Container runtime contract",
      "publisher": "Google Cloud",
      "published": null,
      "updated": "2026-10-30",
      "url": "https://docs.cloud.google.com/run/docs/container-contract",
      "accessed": "2026-11-07",
      "access_note": "Opened primary documentation; checked ephemeral memory-backed filesystem, job exit behavior and task environment variables."
    },
    {
      "id": "S5",
      "title": "Request preconditions",
      "publisher": "Google Cloud",
      "published": null,
      "updated": "2026-10-30",
      "url": "https://docs.cloud.google.com/storage/docs/request-preconditions",
      "accessed": "2026-11-07",
      "access_note": "Opened primary documentation; checked generation matching and zero-generation create-only behavior."
    },
    {
      "id": "S6",
      "title": "GPU-accelerated video transcoding with FFmpeg on Cloud Run jobs",
      "publisher": "Google Cloud",
      "published": null,
      "updated": "2026-10-30",
      "url": "https://docs.cloud.google.com/run/docs/tutorials/video-encoding",
      "accessed": "2026-11-07",
      "access_note": "Opened official tutorial. Used as evidence of a concrete adjacent video workflow, not as proof of the performance of this CPU thumbnail recipe."
    }
  ],
  "facts": [
    {
      "id": "F1",
      "claim": "Cloud Run jobs run containers to completion, can have up to 10,000 independent tasks, and expose task index/count for application partitioning.",
      "source_ids": [
        "S1"
      ],
      "evidence_type": "current_documentation"
    },
    {
      "id": "F2",
      "claim": "Regular jobs in us-central1 list default rates of USD 0.000018/vCPU-second and USD 0.000002/GiB-second. Each started instance has a one-minute minimum.",
      "source_ids": [
        "S2"
      ],
      "evidence_type": "current_published_price_not_transaction_quote"
    },
    {
      "id": "F3",
      "claim": "Cloud Run free-tier usage is aggregated across projects by billing account.",
      "source_ids": [
        "S2"
      ],
      "evidence_type": "current_pricing_terms"
    },
    {
      "id": "F4",
      "claim": "Google recommends idempotent jobs and separate input/output locations for safe restarts.",
      "source_ids": [
        "S3"
      ],
      "evidence_type": "current_documentation"
    },
    {
      "id": "F5",
      "claim": "The default writable container filesystem consumes instance memory and does not persist after the instance stops.",
      "source_ids": [
        "S4"
      ],
      "evidence_type": "current_documentation"
    },
    {
      "id": "F6",
      "claim": "A Cloud Storage generation-match precondition of zero permits a write only when no live destination object exists.",
      "source_ids": [
        "S5"
      ],
      "evidence_type": "current_documentation"
    },
    {
      "id": "F7",
      "claim": "Google provides an official FFmpeg video-transcoding tutorial using GPUs on Cloud Run Jobs.",
      "source_ids": [
        "S6"
      ],
      "evidence_type": "primary_implementation_example_not_independently_reproduced"
    }
  ],
  "inferences": [
    {
      "id": "I1",
      "claim": "Task grouping affects the effective cost of a short-record workload because the minimum applies per started instance.",
      "basis_fact_ids": [
        "F1",
        "F2"
      ],
      "label": "PCT engineering interpretation"
    },
    {
      "id": "I2",
      "claim": "The one-clip layout costs USD 1.20 gross compute; the twenty-clip layout costs USD 0.405; the difference is USD 0.795, or 66.25%.",
      "basis_fact_ids": [
        "F2"
      ],
      "label": "illustrative arithmetic, not measured result",
      "assumptions": [
        "1,000 clips",
        "1 vCPU and 1 GiB per instance",
        "20 seconds per clip including download, processing and upload",
        "5 additional lifetime seconds of overhead per task",
        "No failures, retries, sidecars or overlapping executions",
        "50 tasks with 20 clips each in the bundled layout",
        "Regular us-central1 default USD pricing; free tier and other line items excluded"
      ]
    },
    {
      "id": "I3",
      "claim": "Completed per-clip output archives let a retry skip accepted clips without rerunning the whole bundle.",
      "basis_fact_ids": [
        "F4",
        "F6"
      ],
      "label": "PCT proposed implementation; cloud path not executed"
    },
    {
      "id": "I4",
      "claim": "A 1 GiB memory request is only a trial configuration; decoder use and filesystem usage must be measured on representative inputs.",
      "basis_fact_ids": [
        "F5"
      ],
      "label": "PCT implementation caution"
    },
    {
      "id": "I5",
      "claim": "Per-second price comparisons should account for task size, minimum charges and acceptance criteria.",
      "label": "PCT market interpretation"
    }
  ],
  "unknowns": [
    "Actual Cloud Run runtime, startup/teardown overhead, image quality, memory usage and cost for a production collection.",
    "Live project quotas, IAM policies, regional capacity, storage classes and network path for any prospective reader.",
    "Whether the proposed Docker image builds successfully in the reader's environment; no cloud image build or deployment was performed.",
    "Cloud object download/upload, authentication and retry behavior of this exact implementation; only its local extraction and sharding paths were executed.",
    "The all-in invoice after free allowances, taxes, storage, object operations, build, registry, logs and data-transfer charges.",
    "Production handling of corrupt clips: the minimal example fails a task and requires additional quarantine/reconciliation logic."
  ],
  "correction_history": [],
  "license_status": "Original PCT editorial text; public license not selected. Source rights remain separate.",
  "public_url": null,
  "desk_extensions": {
    "id": "pct-2026-11-07-spotlight-cloud-run-jobs",
    "title": "Cloud Run Jobs: a thumbnail factory with a one-minute meter",
    "section": "Product spotlight",
    "publisher": "Peterson Compute Trading",
    "editorial_voice": "Spotlight Desk | AI-assisted, builder-focused",
    "as_of_date": "2026-11-07",
    "status": "desk_research_with_local_functional_smoke_test",
    "subject": "Google Cloud Run Jobs for CPU video-thumbnail extraction",
    "deck": "A 1,000-clip planning case shows why task size belongs beside the per-second compute price.",
    "editorial_word_count": 1080,
    "reading_minutes": 4.9,
    "cost_model": {
      "currency": "USD",
      "region": "us-central1",
      "pricing_mode": "regular jobs, default on-demand rates",
      "as_of": "2026-11-07",
      "vcpus_per_instance": 1,
      "gib_per_instance": 1,
      "cpu_rate_per_vcpu_second": 1.8e-05,
      "memory_rate_per_gib_second": 2e-06,
      "minimum_seconds_per_instance": 60,
      "records": 1000,
      "assumed_record_seconds": 20,
      "assumed_overhead_seconds_per_task": 5,
      "single_record_tasks": {
        "tasks": 1000,
        "assumed_lifetime_seconds_each": 25,
        "billed_seconds_each": 60,
        "total_billed_seconds": 60000,
        "gross_compute_usd": 1.2
      },
      "bundled_tasks": {
        "tasks": 50,
        "records_per_task": 20,
        "assumed_lifetime_seconds_each": 405,
        "total_billed_seconds": 20250,
        "gross_compute_usd": 0.405
      },
      "gross_savings_usd": 0.795,
      "gross_savings_percent": 66.25,
      "exclusions": "Free allowances, taxes, storage, object operations, Cloud Build, Artifact Registry, logging, network charges, retries, sidecars, operator labor; hypothetical timing is not a benchmark."
    },
    "runnable_workload": {
      "purpose": "Runnable local smoke mode and proposed Cloud Run deployment recipe. Cloud setup and execution are reader actions, not performed here.",
      "files": {
        "Dockerfile": "FROM python:3.12-slim-bookworm\nRUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY main.py manifest.jsonl ./\nCMD [\"python\", \"-u\", \"main.py\"]\n",
        ".gcloudignore": ".git\n__pycache__/\n*.pyc\nfixture.mp4\nthumbnails.tar\nREADME.md\n",
        "main.py": "\"\"\"CPU-only thumbnail batches. Local smoke mode does not use Google Cloud.\"\"\"\nimport argparse\nimport hashlib\nimport json\nimport os\nfrom pathlib import Path\nimport subprocess\nimport tarfile\nimport tempfile\n\nMAX_BYTES = 50 * 1024 * 1024\n\n\ndef assigned_rows(rows, index, count):\n    if count < 1 or not 0 <= index < count:\n        raise ValueError(\"Invalid task index/count\")\n    return rows[index::count]\n\n\ndef make_archive(input_path, output_path):\n    input_path, output_path = Path(input_path), Path(output_path)\n    if input_path.stat().st_size > MAX_BYTES:\n        raise ValueError(\"Example accepts clips no larger than 50 MiB\")\n    probe = json.loads(subprocess.check_output([\n        \"ffprobe\", \"-v\", \"error\", \"-show_entries\", \"format=duration\",\n        \"-of\", \"json\", str(input_path)\n    ]))\n    duration = float(probe[\"format\"][\"duration\"])\n    if not 5 <= duration <= 60.5:\n        raise ValueError(\"Example accepts clips between 5 and 60 seconds\")\n    with tempfile.TemporaryDirectory() as work:\n        pattern = str(Path(work) / \"frame-%04d.jpg\")\n        subprocess.run([\n            \"ffmpeg\", \"-nostdin\", \"-hide_banner\", \"-loglevel\", \"error\",\n            \"-threads\", \"1\", \"-i\", str(input_path), \"-map\", \"0:v:0\",\n            \"-vf\", \"fps=1/5,scale=320:-2\", \"-an\", \"-threads\", \"1\",\n            \"-q:v\", \"3\", pattern\n        ], check=True, timeout=120)\n        frames = sorted(Path(work).glob(\"frame-*.jpg\"))\n        if not frames:\n            raise ValueError(\"No thumbnails produced\")\n        with tarfile.open(output_path, \"w\") as archive:\n            for frame in frames:\n                archive.add(frame, arcname=frame.name)\n        return len(frames)\n\n\ndef run_cloud():\n    from google.cloud import storage\n    from google.api_core.exceptions import PreconditionFailed\n\n    rows = [json.loads(line) for line in Path(\"manifest.jsonl\").read_text().splitlines() if line.strip()]\n    index = int(os.getenv(\"CLOUD_RUN_TASK_INDEX\", \"0\"))\n    count = int(os.getenv(\"CLOUD_RUN_TASK_COUNT\", \"1\"))\n    selected = assigned_rows(rows, index, count)\n    client = storage.Client()\n    output_bucket = client.bucket(os.environ[\"OUTPUT_BUCKET\"])\n    # Change RECIPE_ID whenever code, dependencies, or transform parameters change.\n    recipe_id = os.environ[\"RECIPE_ID\"]\n    for row in selected:\n        generation = int(row[\"generation\"])\n        identity = json.dumps([row[\"bucket\"], row[\"name\"], generation, recipe_id])\n        key = hashlib.sha256(identity.encode()).hexdigest()\n        output = output_bucket.blob(f\"thumbnails/{key}.tar\")\n        if output.exists():\n            print(json.dumps({\"id\": key, \"status\": \"already_complete\"}), flush=True)\n            continue\n        source = client.bucket(row[\"bucket\"]).blob(row[\"name\"], generation=generation)\n        source.reload()\n        if source.size > MAX_BYTES:\n            raise ValueError(f\"Input larger than 50 MiB: {row['name']}\")\n        with tempfile.TemporaryDirectory() as work:\n            input_path = Path(work) / \"input.mp4\"\n            output_path = Path(work) / \"thumbnails.tar\"\n            source.download_to_filename(str(input_path), if_generation_match=generation)\n            frames = make_archive(input_path, output_path)\n            output.metadata = {\"recipe_id\": recipe_id, \"source_generation\": str(generation)}\n            try:\n                output.upload_from_filename(str(output_path), content_type=\"application/x-tar\", if_generation_match=0)\n                status = \"created\"\n            except PreconditionFailed:\n                # Another attempt already committed this deterministic result name.\n                status = \"already_complete\"\n            print(json.dumps({\"id\": key, \"frames\": frames, \"status\": status}), flush=True)\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--local-input\")\n    parser.add_argument(\"--local-output\", default=\"thumbnails.tar\")\n    args = parser.parse_args()\n    if args.local_input:\n        print(json.dumps({\"frames\": make_archive(args.local_input, args.local_output)}))\n    else:\n        run_cloud()\n",
        "README.md": "# Cloud Run Jobs thumbnail batch\n\nThis is an editorial implementation example, not a deployed service. The local FFmpeg path and task sharding were tested. The Docker build, Google authentication, IAM, object transfers, and Cloud Run execution were not tested. Running the cloud commands below can incur charges; none was run in preparing the article.\n\n## Workload\n\nTake rights-cleared MP4 clips, each 5–60 seconds and no larger than 50 MiB. Sample one JPEG every five seconds at 320 pixels wide, preserving aspect ratio. Upload one complete TAR archive per source clip to a separate output bucket. The number of frames depends on input timestamps and duration; a synthetic 60-second CFR fixture produced 12.\n\nThe default container filesystem is memory-backed. The 1 GiB example is a starting configuration, not a proven memory bound for arbitrary codecs or resolutions. The code processes one clip at a time. Profile your actual inputs, including decoder memory, before raising the input limit or running a full collection.\n\n## Local smoke test without Google Cloud\n\nRequires Python 3 and FFmpeg installed. From this directory:\n\n```sh\nffmpeg -nostdin -hide_banner -loglevel error -y \\\n  -f lavfi -i 'testsrc2=size=640x360:rate=30' -t 60 \\\n  -c:v libx264 -preset ultrafast -pix_fmt yuv420p fixture.mp4\npython main.py --local-input fixture.mp4 --local-output thumbnails.tar\ntar -tf thumbnails.tar\n```\n\nDo not use this smoke-test timing as a Cloud Run benchmark. The cloud image uses Debian Bookworm's FFmpeg package, whereas the editorial test used FFmpeg 7.1.5 in the local workspace.\n\n## Cloud prerequisites\n\nUse an existing billing-enabled Google Cloud project, enabled Cloud Run/Cloud Build/Artifact Registry APIs, an existing Docker Artifact Registry repository, and authorized input/output buckets. Use us-central1 for the article's price example. Read the current provider pricing and storage/network charges before execution.\n\nUse an existing dedicated Cloud Run service account authorized to read the input objects and to read/create output objects. Bucket-scoped Storage Object Viewer on inputs, and Storage Object Viewer plus Storage Object Creator on outputs, are one least-privilege starting point for these operations. Other policies, such as CMEK, can require additional permissions. Do not give public access. The deployer needs the Cloud Run and service-account-use permissions described in Google's create-jobs guide; the build identity needs the corresponding build/repository permissions. Ask the project administrator to verify these prerequisites rather than granting project-wide administrator roles.\n\nSet these shell variables to your own verified resources:\n\n```sh\nexport PROJECT='YOUR_PROJECT_ID'\nexport REGION='us-central1'\nexport REPO='YOUR_EXISTING_ARTIFACT_REGISTRY_REPOSITORY'\nexport INPUT_BUCKET='YOUR_INPUT_BUCKET'\nexport OUTPUT_BUCKET='YOUR_OUTPUT_BUCKET'\nexport INPUT_PREFIX='clips/'\nexport SERVICE_ACCOUNT='YOUR_EXISTING_SERVICE_ACCOUNT_EMAIL'\nexport IMAGE=\"$REGION-docker.pkg.dev/$PROJECT/$REPO/pct-thumbnails:nov07-v1\"\nexport RECIPE_ID='nov07-v1'\n```\n\nPrepare `manifest.jsonl` from your authorized input bucket. `make_manifest.py` uses Google Application Default Credentials, which you must configure using your normal approved authentication process. It lists only MP4 objects under INPUT_PREFIX. Review the resulting file; it is embedded in the container image, so the image's access controls must protect any non-public object names.\n\n```sh\npython -m pip install -r requirements.txt\npython make_manifest.py > manifest.jsonl\n```\n\nFor the worked example the manifest has exactly 1,000 distinct clips. For an initial cloud test use a small representative manifest and set the task count accordingly. Keep object generations available for the execution's duration. A missing or replaced generation is an error, not permission to silently process another version.\n\nBuild and create the job (this uploads the source and manifest, stores an image, and creates billable cloud resources):\n\n```sh\ngcloud builds submit --project=\"$PROJECT\" --tag=\"$IMAGE\" .\ngcloud run jobs create pct-thumbnails \\\n  --project=\"$PROJECT\" --region=\"$REGION\" --image=\"$IMAGE\" \\\n  --service-account=\"$SERVICE_ACCOUNT\" \\\n  --cpu=1 --memory=1Gi \\\n  --tasks=50 --parallelism=10 \\\n  --task-timeout=15m --max-retries=1 \\\n  --set-env-vars=\"OUTPUT_BUCKET=$OUTPUT_BUCKET,RECIPE_ID=$RECIPE_ID\"\n```\n\nThen execute explicitly:\n\n```sh\ngcloud run jobs execute pct-thumbnails \\\n  --project=\"$PROJECT\" --region=\"$REGION\" --wait\n```\n\nA Cloud Run task reads CLOUD_RUN_TASK_INDEX and CLOUD_RUN_TASK_COUNT. With 1,000 rows and 50 tasks, each task receives 20 rows by strided partitioning. `--parallelism=10` caps concurrent task instances, not records per task. With a different manifest length the counts can differ by one. Setting more tasks than records creates useless empty tasks and can still incur minimum billing.\n\n## Completion and retries\n\nOutput names hash the input bucket/name/generation and RECIPE_ID. Change RECIPE_ID for every change to code, dependencies, or transform parameters, and use a unique image tag or digest. Output objects are created with `if_generation_match=0`, avoiding overwrite by competing attempts. A retry checks completed objects and skips them. Use an output namespace controlled by this pipeline; a pre-existing object under that deterministic name is considered completed.\n\nThe example fails a task on an invalid clip. A production system should record/quarantine permanent input failures and reconcile the full manifest against accepted outputs; otherwise one corrupt clip can prevent later clips in its shard from being attempted even after a retry. Do not equate a count of log lines with acceptance. Verify archive contents and dimensions, preserve error details, and measure actual billed instance time, output coverage, retries, and peak memory.\n\nThe example intentionally does not create a schedule, change IAM, create credentials, or include destructive cleanup commands. Stored images/objects can continue to incur charges after the job stops; apply your normal retention process.\n",
        "manifest.example.jsonl": "{\"bucket\":\"YOUR_INPUT_BUCKET\",\"name\":\"clips/example.mp4\",\"generation\":\"REPLACE_WITH_OBJECT_GENERATION\"}\n",
        "make_manifest.py": "\"\"\"Run with authorized Google Application Default Credentials. Read-only.\"\"\"\nimport json\nimport os\nfrom google.cloud import storage\n\nclient = storage.Client()\nbucket = os.environ[\"INPUT_BUCKET\"]\nprefix = os.getenv(\"INPUT_PREFIX\", \"clips/\")\nrows = [\n    {\"bucket\": bucket, \"name\": blob.name, \"generation\": str(blob.generation)}\n    for blob in client.list_blobs(bucket, prefix=prefix)\n    if blob.name.lower().endswith(\".mp4\")\n]\nfor row in sorted(rows, key=lambda row: row[\"name\"]):\n    print(json.dumps(row))\n",
        "requirements.txt": "google-cloud-storage==3.17.1\n"
      },
      "local_validation": {
        "date": "2026-11-07",
        "status": "passed",
        "python_syntax": "main.py and make_manifest.py compile",
        "ffmpeg_version": "7.1.5-0+deb13u1",
        "input": "Synthetic 60-second 640x360 H.264 video at 30 fps",
        "output": "12 JPEGs in one TAR archive; each 320x180",
        "sharding": "1,000 integers split across 50 task indices; exactly 20 each; coverage exact and no duplicates",
        "not_tested": [
          "Docker build",
          "Cloud IAM and authentication",
          "Cloud Storage object I/O",
          "Cloud Run execution",
          "Cloud retries",
          "Throughput",
          "Provider bill"
        ]
      }
    }
  },
  "public_projection": {
    "version": "1.0.0",
    "date": "2026-11-10",
    "changes": [
      "Internal workspace artifact paths omitted where present. Historical license/status fields retain original provenance; current public reuse policy is stated separately."
    ],
    "rights": "PCT reserves rights in original editorial content; third-party rights retained. No general source-data reuse license."
  }
}
