NEWVectors or files. Pick a path.Start →
    TrainingEnhanced

    Processing a Large Media Library

    Run extraction over tens of thousands of files without paying for the same object twice. Price the job before you submit it, keep new uploads flowing in on their own, and move cold collections out of the vector store while they stay searchable.

    video
    image
    audio
    text
    Production

    Why This Matters

    Extraction is the expensive part, and the bill is set by how many objects reach a GPU. A library that grows every day will re-pay for everything it already holds unless the pipeline knows what it has seen.

    import os, json, urllib.request
    from mixpeek import Mixpeek
    client = Mixpeek(api_key=os.environ["MIXPEEK_API_KEY"], namespace="media-library")
    BUCKET_ID = "bkt_your_library"
    BATCH_ID = "bat_the_draft_you_just_created"
    COLLECTION_ID = "col_enriched_media"
    ARCHIVE_COLLECTION_ID = "col_2019_archive"
    CONNECTION_ID = "con_your_s3_connection"
    BASE = "https://api.mixpeek.com/v1"
    HEADERS = {
    "Authorization": f"Bearer {os.environ['MIXPEEK_API_KEY']}",
    "X-Namespace": "media-library",
    "Content-Type": "application/json",
    }
    def api(method, path, body=None):
    """The facade covers buckets, collections, retrievers, documents,
    evaluations, tasks and namespaces. Batch cost, bucket syncs and collection
    lifecycle have no resource on it yet, so those three go over HTTP."""
    req = urllib.request.Request(
    f"{BASE}{path}",
    method=method,
    data=json.dumps(body).encode() if body is not None else None,
    headers=HEADERS,
    )
    with urllib.request.urlopen(req) as r:
    return json.load(r)
    # 1. Price the run before you spend anything. estimate-cost consumes no credits
    # and changes nothing about the batch. already_extracted_count is the number
    # you care about: under 'force' or 'replace' those objects go back through a
    # GPU and estimated_repay_credits is what that costs.
    est = api("POST", f"/buckets/{BUCKET_ID}/batches/{BATCH_ID}/estimate-cost")
    print(est["object_count"], "objects,", est["already_extracted_count"], "already done")
    print(est["estimated_credits"], "credits")
    if est.get("estimated_usd") is not None:
    print("approx USD:", round(est["estimated_usd"], 2))
    for w in est.get("warnings", []):
    print("warning:", w)
    # 2. Run it. dedup_strategy defaults to 'skip' and is scoped to
    # (bucket, collection), so re-triggering after the bucket grows pays only for
    # the new objects. Reach for 'replace' when the extractor itself changed and
    # the old documents are wrong; 'force' allows duplicates and is rarely what
    # you want.
    client.collections.trigger(COLLECTION_ID, dedup_strategy="skip")
    # 3. Stop triggering by hand. A sync watches the bucket's storage connection and
    # submits what lands. skip_duplicates is on by default and does the same job
    # as dedup_strategy for objects arriving continuously.
    sync = api("POST", f"/buckets/{BUCKET_ID}/syncs", {
    "connection_id": CONNECTION_ID,
    "source_path": "incoming/",
    "sync_mode": "continuous",
    "polling_interval_seconds": 300,
    "batch_size": 50,
    "skip_duplicates": True,
    })
    # 4. Keep the hot set small. 'cold' evicts a collection's vectors from the vector
    # store and they stay searchable from object storage; 'active' rehydrates.
    # Search latency is set by what is resident, so an archive that nobody queries
    # should not be.
    api("PATCH", f"/collections/{ARCHIVE_COLLECTION_ID}/lifecycle",
    {"lifecycle_state": "cold"})

    Feature Extractors

    Multimodal Extractor

    Unified embeddings for video, audio, image, and text: scene/silence chunking, Whisper transcription, thumbnails, and Gemini vision.

    Retriever Stages

    feature search

    Search and filter documents by vector similarity using feature embeddings

    filter

    Frequently asked questions

    How do I process thousands of videos without re-paying for the ones already done?

    Leave dedup_strategy on its default of 'skip'. It is scoped to (bucket, collection), so an object counts as done if the target collection already holds documents produced from it, whichever batch made them. Re-trigger after the bucket grows and only the new objects reach a GPU. Before submitting anything, POST to the batch's estimate-cost endpoint: it returns object_count, already_extracted_count and estimated_credits without consuming credits or changing the batch.

    What actually drives the cost of processing a large library?

    How many objects reach an extractor, and which features you ask for. The cost estimate breaks this out as content_summary (file types and how much of each, including video minutes) and features (what the batch will let you search by). If you are using replace or force, estimated_repay_credits is the part of the bill that buys you nothing new.

    How do I process new uploads automatically as they arrive?

    Create a sync on the bucket. With sync_mode "continuous" it polls the storage connection every polling_interval_seconds (300 by default) and submits what it finds in batches of batch_size (50 by default). skip_duplicates defaults to true. max_objects_per_run caps a single run if you want a hard ceiling while you are still tuning it.

    How do I keep search fast once the library is large?

    Shrink what is resident. PATCH a collection's lifecycle to "cold" and its vectors leave the vector store while staying searchable through object storage; "active" brings them back. Latency tracks the size of the hot set, so a five-year archive that nobody queries should not be sitting in it.

    Can I do all of this from the Python SDK?

    Partly. client.collections.trigger(collection_id, dedup_strategy=...) is on the client. Batch cost estimation, bucket syncs and collection lifecycle have no resource manager on it yet, so those three are plain HTTP calls against the same API key.