T09-L04

Create media · Integrator

Self-hosted media models

At Level 4 Integrator, "run it locally" is not a complete architecture. You must identify the exact model and workflow, prove that peak memory fits, measure useful throughput on representative inputs, account for the machine and your operating time, and preserve the same rights, review, accessibility, provenance,...

Level
IntegratorLevel 4 of 5
Curriculum position
Family 1 · Track 09
Reading time
90 minutes
Reading progress
0%Time on this book
Last revised
Sep 5, 2026

At Level 4 Integrator, "run it locally" is not a complete architecture. You must identify the exact model and workflow, prove that peak memory fits, measure useful throughput on representative inputs, account for the machine and your operating time, and preserve the same rights, review, accessibility, provenance, and release controls used for hosted media.

2. One hundred renders and a tempting multiplication

You need one hundred short renders for a synthetic asset set. The hosted service displays a per-clip price, and multiplying it by one hundred produces a number larger than a month of advertised GPU rental. You start downloading a model before checking its peak VRAM, license, output dimensions, or likely generation time. The rented machine keeps billing while you debug drivers and copy outputs.

In the Lab version, recordings cannot leave institute-controlled infrastructure, so local Whisper seems obviously right. Yet "local" turns out to mean a workstation with a shared disk, broad user access, automatic model downloads, and transcripts retained in a debug folder. The route is not controlled merely because no transcription API was called.

You pause and run the comparison in the right order: can the exact workload fit with headroom, is measured throughput fast enough, and is the full self-hosted cost lower for the decision period? Only then do you choose local hardware, a time-limited rented GPU, or the approved hosted route.

3. After this you can

  • Run one image, video, or speech model on a controlled GPU route and retain reproducible evidence.
  • Estimate whether model weights plus runtime memory, activations, and media dimensions fit available VRAM.
  • Measure cold start, warm generation time, peak VRAM, failures, and useful throughput on representative fixtures.
  • Compare hosted charges with rental or owned-hardware cost plus setup, operations, storage, transfer, review, and your time.
  • Decide when self-hosting wins for control, volume, or offline work and when it is not worth operating.

4. Prerequisites

  • T09-L03 · Media pipelines, including manifests, source hashes, stable IDs, glossary checks, review gates, captions, provenance, and cost estimation.
  • T11-L04 · Your own AI server, including server provisioning, one TLS front door, staging, production separation, patch ownership, and restricted ports.
  • One approved NVIDIA GPU workstation or an owner-approved time-limited GPU rental. Other accelerators are valid only when the selected runtime documents support and you can reproduce the same measurements.
  • Permission to install the pinned runtime and download the exact model snapshot from its primary source into an approved cache. Record model license and intended-use review before generation.
  • Ten short synthetic audio fixtures from T09-L03 for the Lab lane, or ten synthetic prompts and approved reference-free style constraints for the Company lane.
  • A named infrastructure owner, media reviewer, budget owner, shutdown control, and about 90 minutes.

Use only public, synthetic, or explicitly approved media. Do not place collaborator recordings, participant speech, customer calls, unreleased campaigns, identity references, credentials, or private prompts on a personal workstation or unapproved GPU rental. A rented GPU is still a third-party data route. Confirm region, disk lifecycle, snapshots, logs, support access, transfer, and deletion before approved sensitive use.

5. The idea in one page

Use three gates in order:

1. FITS?  model + runtime + workload peak < usable VRAM with headroom
       no -> smaller approved model/workload, offload with a new speed test, or hosted route
       yes
        |
2. FAST ENOUGH?  measured useful outputs / wall-clock hour meets the deadline
       no -> change the approved configuration or stop
       yes
        |
3. CHEAPER OR BETTER CONTROL?  full decision-period cost and risk beat alternatives
       no -> use the approved hosted route
       yes -> operate the pinned self-hosted route with review and shutdown controls

Fit is a peak-memory question, not a download-size question. Model weights occupy memory, but so do the runtime, text encoder, decoder or VAE, attention workspace, activations, audio buffers, video frames, and concurrent jobs. Precision, quantization, resolution, frame count, batch size, sampler, and offloading change the peak. Driver tools report observed allocation; model documentation gives only a planning estimate. Reserve headroom for the operating display and runtime rather than targeting the last available megabyte.

Speed is measured on the final workload. Record cold start separately from warm runs. For speech, report processed audio minutes per wall-clock minute and real-time factor: elapsed seconds / audio seconds. For images, report accepted images per wall-clock hour at the required dimensions and workflow. For video, record seconds of reviewed output per wall-clock hour at the required frame count, dimensions, and frame rate. A tiny preview or cached rerun is not evidence for the production-shaped batch.

Cheap means total cost over a named period. Compare the same output definition and review standard:

hosted total = usage charges + transfer + storage + integration time + operations time
self-hosted rental = billed GPU hours + attached storage + transfer + setup time + operations time
self-hosted owned = allocated hardware/depreciation + electricity + storage + setup + operations

Add human time at the organisation's agreed loaded rate. Add review and correction to both routes unless measurement shows a difference. Do not treat an already-owned GPU as free: allocate electricity, unavailable use by others, maintenance, and an agreed hardware share. Do not charge the entire purchase to a one-week pilot either; state the accounting period and method.

Self-hosting can clearly win when approved material must remain on institute-controlled infrastructure, the workload is high and steady, offline operation matters, or a fixed workflow needs predictable control. Hosted service can win for small or spiky volumes, fast-changing capabilities, limited operations capacity, or models too large for available hardware. A rental can test fit and throughput before purchase, but it adds provider governance and a clock that must be stopped.

The serving location does not grant rights. Review the model code license, weight license, accepted-use terms, reference-media rights, output terms, and intended commercial or publication use separately. "Open weights" describes availability, not permission for every use. Keep human review, captions, disclosure, source mapping, and publication approval from T09-L03 unchanged.

6. The worked example: benchmark before committing

Create one supporting-evidence packet for either lane. The packet makes the work auditable, but it is not a second exit artifact: the required artifact remains the single evidence screenshot specified in the exit check.

self-hosted-media-evidence/
  decision.md
  environment.txt
  model-record.md
  fixtures/manifest.csv
  configuration/
  scripts/
  tests/
  runs/raw/
  runs/summary.csv
  outputs/reviewed/
  review.csv
  screenshot.png

decision.md names the workload, output definition, deadline, decision period, alternatives, owners, approved data class, stop conditions, and review gate. model-record.md contains exact model repository, revision or snapshot identifier, file hashes where available, code and weight licenses, intended-use decision, runtime versions, and download date. A filename such as latest.safetensors is not a reproducible identity.

environment.txt records operating system, CPU, RAM, GPU model, total VRAM, driver, CUDA/runtime versions, storage, and whether the GPU also drives a display. Capture these with the actual platform tools. On NVIDIA systems, begin with:

nvidia-smi --query-gpu=name,driver_version,memory.total,memory.free --format=csv
python --version

If nvidia-smi is unavailable, shows the wrong GPU, or reports less free memory than the approved plan assumes, stop. Do not install random drivers from a forum or begin a paid download while diagnosing the machine.

Make a fit worksheet

Before loading the model, write:

ItemPlanned valueEvidence source
Exact model snapshotRECORD_BEFORE_RUNPrimary model repository and local hash
Runtime and precisione.g. CTranslate2 with float16Pinned environment and runtime documentation
Workloadten 10-20 second WAV filesManifest IDs, hashes, measured durations
Model memory estimateRECORDModel documentation or controlled prior run
Runtime/workload allowanceRECORDSame configuration or conservative pilot
Available VRAMRECORDPre-run hardware query
Required headroomat least the owner-approved marginInfrastructure decision
Fit decisionpass, uncertain, or failOwner before model load

Do not invent a universal VRAM number for "Whisper," "Stable Diffusion," or "video." Variants and implementations differ. If evidence is absent, mark fit uncertain and run only a one-item controlled probe with an automatic stop. An out-of-memory event is a failed fit result, not a cue to repeat until the machine crashes.

Lab lane: transcribe synthetic recordings with local Whisper

This lane reuses the stable IDs and ten short synthetic WAV fixtures from T09-L03. The machine is institute-controlled, its working directory is restricted to the packet, and the run has no network access. Download the approved model snapshot during a separately logged preparation step, verify it, then point the runtime to that local directory. Automatic downloads during processing are disabled by architecture: the script receives a path, not a remote model name.

Create a virtual environment using the organisation's approved process. After compatibility review, pin the chosen packages in requirements.txt. This example uses faster-whisper==1.2.1; the primary project's v1.2.1 release, Catalogued in section 11, records that version as published on 31 October 2025 (checked 4 September 2026):

faster-whisper==1.2.1

Install from the approved package mirror or verified cache:

python -m venv .venv
source .venv/bin/activate
python -m pip install --requirement requirements.txt
python -m pip freeze > environment-lock.txt

On Windows PowerShell, activation is .venv\Scripts\Activate.ps1. Package installation may retrieve code and native libraries; perform it before restricted media enters the workspace. Review the pinned dependency tree and accelerator compatibility. Do not solve a CUDA error by repeatedly installing unrecorded versions.

Save this as scripts/local_transcribe.py:

import argparse
import hashlib
import json
import platform
import subprocess
import time
from pathlib import Path

def sha256(path):
    digest = hashlib.sha256()
    with path.open("rb") as source:
        for block in iter(lambda: source.read(1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()

def gpu_record():
    command = [
        "nvidia-smi",
        "--query-gpu=name,driver_version,memory.total,memory.used",
        "--format=csv,noheader,nounits",
    ]
    try:
        line = subprocess.run(
            command, check=True, capture_output=True, text=True, timeout=10
        ).stdout.strip().splitlines()[0]
        name, driver, total, used = [part.strip() for part in line.split(",", 3)]
        return {"gpu": name, "driver": driver, "vram_total_mib": int(total),
                "vram_used_mib_after_run": int(used)}
    except (FileNotFoundError, subprocess.SubprocessError, IndexError, ValueError):
        return {"gpu": "not_observed", "driver": "not_observed",
                "vram_total_mib": None, "vram_used_mib_after_run": None}

def default_factory(model_path, device, compute_type):
    from faster_whisper import WhisperModel
    return WhisperModel(str(model_path), device=device, compute_type=compute_type)

def run(input_path, model_path, output_path, item_id, model_factory=default_factory,
        device="cuda", compute_type="float16"):
    input_path, model_path, output_path = map(Path, (input_path, model_path, output_path))
    if not input_path.is_file():
        raise ValueError("input file does not exist")
    if not model_path.is_dir():
        raise ValueError("local model directory does not exist")
    started = time.perf_counter()
    model = model_factory(model_path, device, compute_type)
    segments, info = model.transcribe(
        str(input_path), language="en", beam_size=1, vad_filter=False
    )
    rows = [
        {"start": round(segment.start, 3), "end": round(segment.end, 3),
         "text": segment.text.strip()}
        for segment in segments
    ]
    elapsed = round(time.perf_counter() - started, 3)
    if not rows or any(row["end"] <= row["start"] or not row["text"] for row in rows):
        raise ValueError("model returned no valid transcript segments")
    result = {
        "item_id": item_id,
        "input_sha256": sha256(input_path),
        "model_path": model_path.name,
        "device": device,
        "compute_type": compute_type,
        "language": info.language,
        "elapsed_seconds": elapsed,
        "segments": rows,
        "status": "review_required",
        "hardware": gpu_record(),
        "platform": platform.platform(),
    }
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
    return result

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--input", required=True)
    parser.add_argument("--model-path", required=True)
    parser.add_argument("--output", required=True)
    parser.add_argument("--item-id", required=True)
    args = parser.parse_args()
    record = run(args.input, args.model_path, args.output, args.item_id)
    print(json.dumps({"item_id": record["item_id"], "status": record["status"],
                      "elapsed_seconds": record["elapsed_seconds"],
                      "gpu": record["hardware"]["gpu"]}))

The script hashes the actual input, measures model load plus transcription, fixes language and decoding settings, writes timestamped segments, and always labels output review_required. It does not send, publish, approve, or delete anything. memory.used after the run is not peak VRAM, so collect peak separately with a one-second monitor or your platform's approved GPU telemetry; never mislabel the final sample as the peak.

Save deterministic tests as tests/test_local_transcribe.py:

import json
import tempfile
import unittest
from pathlib import Path

from scripts.local_transcribe import run

class Segment:
    start, end, text = 0.0, 1.25, " Welcome to fictional Aster-9. "

class Info:
    language = "en"

def fake_factory(model_path, device, compute_type):
    class FakeModel:
        def transcribe(self, path, language, beam_size, vad_filter):
            return iter([Segment()]), Info()
    return FakeModel()

class LocalTranscribeTests(unittest.TestCase):
    def test_writes_source_linked_review_record(self):
        with tempfile.TemporaryDirectory() as folder:
            root = Path(folder)
            audio, model, output = root / "fixture.wav", root / "model", root / "out.json"
            audio.write_bytes(b"synthetic-audio-placeholder")
            model.mkdir()
            result = run(audio, model, output, "LAB-SYN-001", fake_factory, "cpu", "int8")
            saved = json.loads(output.read_text(encoding="utf-8"))
            self.assertEqual(result["status"], "review_required")
            self.assertEqual(saved["item_id"], "LAB-SYN-001")
            self.assertEqual(saved["segments"][0]["text"], "Welcome to fictional Aster-9.")
            self.assertEqual(len(saved["input_sha256"]), 64)

    def test_missing_input_stops_before_model(self):
        with tempfile.TemporaryDirectory() as folder:
            root = Path(folder)
            model = root / "model"
            model.mkdir()
            with self.assertRaisesRegex(ValueError, "input file does not exist"):
                run(root / "missing.wav", model, root / "out.json", "MISSING", fake_factory)

if __name__ == "__main__":
    unittest.main()

Run from the packet root before loading model weights:

python -m unittest -v tests/test_local_transcribe.py

Expected ending:

Ran 2 tests

OK

Then run one real synthetic fixture, substituting the verified local snapshot directory:

python scripts/local_transcribe.py \
  --input fixtures/LAB-SYN-001.wav \
  --model-path models/APPROVED_WHISPER_SNAPSHOT \
  --output runs/raw/LAB-SYN-001.json \
  --item-id LAB-SYN-001

Expected output shape, with measured values replacing placeholders:

{"item_id": "LAB-SYN-001", "status": "review_required", "elapsed_seconds": 3.417, "gpu": "OBSERVED GPU NAME"}

If CUDA libraries are unavailable or incompatible, the run should fail before producing approved media. Record the exact error, driver, runtime, and package lock. Do not silently switch to CPU and compare that result with a planned GPU route. A deliberate CPU benchmark is valid only as a separately named configuration. If an out-of-memory error occurs, mark the configuration fit: fail; reduce the approved model, precision, concurrency, or workload one variable at a time and rerun the full pilot under a new configuration ID.

Run all ten fixtures one at a time first. Capture wall-clock elapsed time, source audio duration from the manifest, peak VRAM telemetry, output size, and status. Compute:

real-time factor = total elapsed seconds / total source-audio seconds
audio throughput = total source-audio minutes / total wall-clock minutes
acceptance yield = outputs approved after review / attempted outputs

Listen to every fixture while reviewing the transcript, names, quantities, negation, omissions, and timestamps. A fast wrong transcript has zero useful throughput until corrected. Preserve the raw result and put only reviewed output under outputs/reviewed/.

Company lane: run a fixed ComfyUI image workflow

For the fictional high-volume asset set, use a pinned ComfyUI commit or approved immutable image, an exact approved checkpoint, and one exported API-format workflow JSON. This baseline pins ComfyUI v0.34.0 to commit 12d5279438bfefc058a269eae805ceab6047777f; the primary repository's release and tag were checked on 4 September 2026. Clone it during the approved networked preparation step, then create the environment using the organisation's reviewed dependency process:

git clone https://github.com/Comfy-Org/ComfyUI.git configuration/ComfyUI
git -C configuration/ComfyUI checkout 12d5279438bfefc058a269eae805ceab6047777f
git -C configuration/ComfyUI rev-parse HEAD
python -m venv configuration/ComfyUI/.venv
configuration/ComfyUI/.venv/bin/python -m pip install --requirement configuration/ComfyUI/requirements.txt
configuration/ComfyUI/.venv/bin/python -m pip freeze > configuration/comfy-environment-lock.txt

On PowerShell, the environment's interpreter is configuration\ComfyUI\.venv\Scripts\python.exe. The rev-parse output must equal the pinned commit. An organisation may approve another revision, but must replace the revision everywhere, record its primary-source URL and review date, and rerun the tests rather than silently following the default branch.

Place the reviewed checkpoint under configuration/ComfyUI/models/checkpoints/APPROVED_CHECKPOINT.safetensors, record its source revision, license, and SHA-256 in model-record.md, and replace RECORDED_SHA256 below with that 64-character digest. The generic filename is intentional: it does not assert that any particular weights are approved. Start the service bound to loopback only unless T11-L04's authenticated TLS gateway is already in place:

configuration/ComfyUI/.venv/bin/python configuration/ComfyUI/main.py \
  --listen 127.0.0.1 --port 8188 --disable-auto-launch

Do not expose port 8188 publicly. ComfyUI is the workflow surface, not the access-control boundary. Record every custom node repository and commit; custom nodes execute code and change the supply chain. This benchmark uses only built-in nodes.

Save this built-in-node API workflow as configuration/company-workflow-api.json:

{
  "3":{"class_type":"KSampler","inputs":{"cfg":7,"denoise":1,"latent_image":["5",0],"model":["4",0],"negative":["7",0],"positive":["6",0],"sampler_name":"euler","scheduler":"normal","seed":1,"steps":20}},
  "4":{"class_type":"CheckpointLoaderSimple","inputs":{"ckpt_name":"APPROVED_CHECKPOINT.safetensors"}},
  "5":{"class_type":"EmptyLatentImage","inputs":{"batch_size":1,"height":512,"width":512}},
  "6":{"class_type":"CLIPTextEncode","inputs":{"clip":["4",1],"text":"SYNTHETIC_PROMPT"}},
  "7":{"class_type":"CLIPTextEncode","inputs":{"clip":["4",1],"text":"person, face, logo, trademark, text, watermark"}},
  "8":{"class_type":"VAEDecode","inputs":{"samples":["3",0],"vae":["4",2]}},
  "9":{"class_type":"SaveImage","inputs":{"filename_prefix":"review_required/ITEM_ID","images":["8",0]}}
}

Save the copy-pasteable queue client as scripts/queue_comfy.py:

import argparse
import hashlib
import json
from pathlib import Path
from urllib import request

def sha256(path):
    digest = hashlib.sha256()
    with Path(path).open("rb") as source:
        for block in iter(lambda: source.read(1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()

def prepare(workflow_path, item_id, prompt, seed):
    workflow = json.loads(Path(workflow_path).read_text(encoding="utf-8"))
    expected = {"3": "KSampler", "4": "CheckpointLoaderSimple",
                "5": "EmptyLatentImage", "6": "CLIPTextEncode",
                "7": "CLIPTextEncode", "8": "VAEDecode", "9": "SaveImage"}
    if {key: workflow.get(key, {}).get("class_type") for key in expected} != expected:
        raise ValueError("workflow nodes differ from the reviewed baseline")
    if not item_id.startswith("COMP-SYN-") or not prompt.strip():
        raise ValueError("invalid synthetic item")
    workflow["3"]["inputs"]["seed"] = seed
    workflow["6"]["inputs"]["text"] = prompt
    workflow["9"]["inputs"]["filename_prefix"] = f"review_required/{item_id}"
    return workflow

def queue(workflow, opener=request.urlopen):
    body = json.dumps({"prompt": workflow}).encode("utf-8")
    response = opener(request.Request("http://127.0.0.1:8188/prompt", data=body), timeout=10)
    return json.loads(response.read().decode("utf-8"))

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--workflow", required=True)
    parser.add_argument("--checkpoint", required=True)
    parser.add_argument("--checkpoint-sha256", required=True)
    parser.add_argument("--item-id", required=True)
    parser.add_argument("--prompt", required=True)
    parser.add_argument("--seed", required=True, type=int)
    args = parser.parse_args()
    if sha256(args.checkpoint) != args.checkpoint_sha256.lower():
        raise SystemExit("checkpoint hash mismatch")
    result = queue(prepare(args.workflow, args.item_id, args.prompt, args.seed))
    print(json.dumps({"prompt_id": result["prompt_id"], "status": "review_required"}))

Save tests/test_queue_comfy.py:

import json
import unittest
from scripts.queue_comfy import prepare, queue

class Response:
    def read(self):
        return b'{"prompt_id":"synthetic-test-id"}'

class QueueComfyTests(unittest.TestCase):
    def test_prepares_source_linked_job_without_network(self):
        workflow = prepare("configuration/company-workflow-api.json",
                           "COMP-SYN-001", "abstract blue geometric field", 41001)
        self.assertEqual(workflow["3"]["inputs"]["seed"], 41001)
        self.assertEqual(workflow["9"]["inputs"]["filename_prefix"],
                         "review_required/COMP-SYN-001")
        sent = {}
        def fake_open(req, timeout):
            sent.update(json.loads(req.data))
            return Response()
        self.assertEqual(queue(workflow, fake_open)["prompt_id"], "synthetic-test-id")
        self.assertEqual(sent["prompt"]["6"]["inputs"]["text"],
                         "abstract blue geometric field")

if __name__ == "__main__":
    unittest.main()

From the packet root, run the deterministic test before starting ComfyUI:

python -m unittest -v tests/test_queue_comfy.py

Then queue the first synthetic item after the loopback service is ready:

python scripts/queue_comfy.py \
  --workflow configuration/company-workflow-api.json \
  --checkpoint configuration/ComfyUI/models/checkpoints/APPROVED_CHECKPOINT.safetensors \
  --checkpoint-sha256 RECORDED_SHA256 \
  --item-id COMP-SYN-001 \
  --prompt "abstract blue geometric field, paper-cut style, no text" \
  --seed 41001

The baseline fixes node graph, checkpoint filename, prompt skeleton, sampler, scheduler, steps, guidance, dimensions, batch size, explicit seed, and output prefix derived from item_id. Hash the workflow and resulting output, and retain the checkpoint hash check. Use ten synthetic prompts with no person, trademark, customer product, or unlicensed reference image. Queue one warm-up that is excluded from throughput, then ten recorded items at concurrency one. Every output enters review_required.

Record queue time, generation time, peak VRAM, seed, dimensions, workflow hash, model hash, output hash, and failure for each item. Review against the intended composition, unwanted text, visual defects, style kit, rights record, disclosure, and final channel. Accepted images per hour, not raw files per hour, is the useful measure. Only after a concurrency-one run fits with headroom may you test a larger batch or video workflow.

Video magnifies memory and time through frame dimensions, count, temporal processing, and decoding. For LTX-Video or another approved local video model, begin with one short synthetic clip at the actual delivery settings. Record generated seconds per wall-clock hour and inspect every frame range, motion, consistency, captions, and disclosure. Do not extrapolate a 512-pixel two-second preview to one hundred final clips.

Compare the same decision period

Create runs/summary.csv with one row per configuration:

config_id,route,model_revision,items,accepted,failed,cold_start_s,warm_elapsed_s,peak_vram_mib,gpu_billed_h,service_cost,compute_cost,storage_transfer_cost,setup_h,operations_h,human_rate,total_cost,deadline_met
LAB-FW-1,self_hosted,RECORDED_REVISION,10,RECORD,RECORD,RECORD,RECORD,RECORD,RECORD,0,RECORD,RECORD,RECORD,RECORD,RECORD,RECORD,RECORD
LAB-HOSTED-1,hosted,RECORDED_SERVICE_VERSION,10,RECORD,RECORD,0,RECORD,not_applicable,0,RECORD,0,RECORD,RECORD,RECORD,RECORD,RECORD,RECORD

Use invoices, current approved rates, platform telemetry, and measured time. Keep setup time visible even when it makes the local pilot lose. For a rental, billed time may exceed generation time because provisioning, downloads, review, and debugging happen while the instance runs. For owned hardware, document the allocation formula rather than inserting a rental price.

Scale to one hundred only after the ten-item pilot:

projected warm compute = measured per-item warm time * 100 / measured parallel efficiency
projected accepted outputs = 100 * measured acceptance yield
projected rework = rejected outputs * measured correction or rerun time

Do not assume linear parallel efficiency or 100 percent acceptance. State uncertainty and run a larger pilot if the cost decision changes near the break-even point. Self-host when fit passes, the measured route meets the deadline with headroom, the full cost or control case wins, licensing permits the use, and somebody owns patching, monitoring, access, deletion, and shutdown. Otherwise choose the approved hosted route and retain the comparison.

Capture the required evidence screenshot

Take one screenshot after a successful reviewed output. It must show the locally generated or transcribed synthetic asset, item ID, exact configuration or model revision, measured generation time, observed GPU model and VRAM, review_required or reviewer decision, and no credential, private path, real person, or restricted source text. A terminal alone is insufficient if it does not show the asset; an attractive asset alone is insufficient if it omits hardware and timing. Place the screenshot in the same evidence packet, with a caption and source hash in decision.md.

Shut down and verify deletion

For a rental, stop the model process, copy only approved evidence to the authorised destination, remove working media according to policy, destroy attached disks and snapshots where the provider supports it, terminate the instance, and verify the billing dashboard no longer reports it running. For owned hardware, remove temporary files and revoke temporary accounts or network rules. Record completion time and responsible person. "Closed the browser tab" is not a shutdown control.

7. What goes wrong

The model does not fit after a large download

Symptom: loading or the first final-size item exhausts VRAM even though checkpoint size looked smaller than available memory.

Fix: estimate the complete runtime workload, reserve headroom, and run a one-item probe before the batch. Record out-of-memory as a failed configuration.

Your time is priced at zero

Symptom: the comparison includes API charges and GPU rent but omits setup, dependency repair, review, patching, backups, and shutdown.

Fix: measure human hours and apply the agreed loaded rate to both routes. State the accounting period and uncertainty.

The rented GPU keeps billing

Symptom: the process ended, but the instance, attached disk, or snapshot remains active overnight.

Fix: set a budget alert and automatic expiry before provisioning, use a shutdown checklist, terminate rather than merely disconnect, and verify the billing state.

Open weights are assumed to permit every use

Symptom: a downloadable model enters a commercial asset workflow without review of weight, code, reference, or output terms.

Fix: record exact revisions and licenses, check intended use with the accountable owner, and stop when terms or reference rights are unclear.

A preview benchmark is extrapolated to final video

Symptom: a low-resolution short clip is fast, so one hundred longer delivery-size clips are promised.

Fix: benchmark representative dimensions, frames, settings, and review. Report accepted seconds of output per wall-clock hour.

Local is mistaken for controlled

Symptom: restricted recordings sit on shared disks, automatic downloads enable egress, logs retain transcripts, or a local UI listens on every interface.

Fix: map the whole route, restrict users and storage, stage dependencies before data, deny processing egress, bind to loopback or an approved gateway, set retention, and verify deletion.

Raw throughput replaces useful throughput

Symptom: files per hour look excellent while many outputs fail transcript, visual, accessibility, or rights review.

Fix: retain failures and calculate acceptance yield. Compare accepted, reviewed outputs under the same quality gate for hosted and self-hosted routes.

8. Do it yourself: one local asset in 90 minutes

Minutes 0-10: choose speech, image, or video. Define one representative synthetic fixture, required output, decision period, deadline, data route, reviewers, accepted quality, and shutdown owner. Name the hosted comparator.

Minutes 10-22: select an exact model and runtime revision from primary sources. Record code and weight licenses, intended-use decision, hashes, required precision and workload settings, dependencies, and custom code. Stop if permission is unclear.

Minutes 22-32: inspect hardware and make the fit worksheet. Record available VRAM, estimated model and runtime demand, headroom, storage, and a one-item stop condition. For a rental, configure budget alert, expiry, restricted network, and deletion plan before starting it.

Minutes 32-45: build or restore the pinned environment before adding restricted data. Run deterministic harness tests. Keep the service on loopback, stage the verified model snapshot, then disable processing egress where feasible.

Minutes 45-58: run one cold representative item and at least three warm repetitions with fixed settings. Capture configuration ID, elapsed time, peak VRAM, failures, source and output hashes, and raw output. Do not hide an out-of-memory or failed generation.

Minutes 58-68: review the output against the source or prompt, quality target, accessibility needs, disclosure, and rights record. Correct or rerun through the same tracked pipeline. Calculate useful throughput and acceptance yield.

Minutes 68-78: compare the measured route with the hosted alternative for the named volume and period. Include compute, billing granularity, storage, transfer, setup, operations, review, rework, electricity or hardware allocation, and human time. State whether the deadline is met.

Minutes 78-85: write the decision: self-host, rent for the batch, use hosted service, or gather more evidence. Tie it to fit, speed, cost, control, licensing, and owner capacity. Do not force a self-host decision to make the exercise look successful.

Minutes 85-90: capture the evidence screenshot, stop services, terminate rental resources, verify deletion and billing state, and ask another person to find the asset, model revision, hardware, timing, peak VRAM, review state, total-cost comparison, decision, and shutdown evidence.

9. Exit check

Deliver exactly one artifact: one evidence screenshot of a locally generated or transcribed synthetic asset with its item ID, generation time, hardware, and observed VRAM visible. The self-hosted-media-evidence/ packet is supporting evidence for that screenshot, not another delivered artifact; it retains the source manifest and hashes, exact model and runtime revisions, license and intended-use record, fit worksheet, executable implementation and tests, raw runs, review, cost comparison, route decision, and shutdown records.

It passes when another person can reproduce the configuration from recorded revisions; verify that the model fit with the declared headroom; see at least one local asset tied to its source or prompt, hardware, timing, and output hash; rerun the deterministic tests; distinguish raw from accepted throughput; audit every cost line and assumption; confirm the data route and license support the intended use; and see that no output bypassed human review. Choosing the hosted route after a successful local pilot is a valid passing decision when the evidence says self-hosting is not worth it.

It fails if the screenshot omits the asset, time, or hardware; a model name lacks a revision; checkpoint size is presented as peak VRAM; only a preview was timed; failures or review rejects disappeared; human time was zeroed without justification; restricted media used an unapproved route; open weights were treated as blanket permission; or a rental remains billable. Correct and rerun the same packet rather than assembling a second artifact from selected successes.

10. Rule to remember

Fits, then fast, then cheap - in that order.

11. Further reading & tools