Return to Selected Works
ai/Multimodal AI & Python

Stockflow

“Eliminating mechanical cataloging friction for high-volume photography through strict multimodal schema pipelines.”

An end-to-end multimodal automation suite for batch photo renaming and marketplace metadata generation using Gemini Vision.

Role
Sole Creator & AI Engineer
Context
3 Weeks (Production Tool)
Team
Solo Project
Core Stack
Python 3.11, Google Gemini Vision, API Key Pooling, CSV Engine
Stockflow

Fig 1.0 — Architecture execution snapshot (Stockflow)

The Friction

Why build an AI pipeline for stock photography metadata?

For photographers working in high-volume macro and landscape photography, capturing images is creative, but preparing them for marketplace ingestion is agonizing mechanical labor. Stock agencies like Shutterstock require strict 20–40 keyword tags, two categorized taxonomies, and clear editorial descriptions for every single photograph.

Existing commercial tagging software either uses outdated non-contextual computer vision or requires expensive per-image subscription tokens. I needed an automated CLI pipeline that could inspect high-resolution RAW exports, understand nuanced micro-details (like insect anatomy or botanical lighting), and output 100% compliant marketplace CSV sheets without human intervention.

Deliberate Constraints

The system architecture was not chosen in an unconstrained vacuum. Each structural decision emerged directly from four non-negotiable technical boundaries.

[RATE-LIMIT RESILIENCE]

Free-tier and standard multimodal API endpoints enforce tight RPM (requests per minute) caps on large photo folders.

Architectural Outcome

Engineered a round-robin API key pool (GEMINI_API_KEYS=k1,k2,k3) coupled with exponential backoff retry logic to maintain continuous batch throughput.

[STRICT BRAND IP DENYLIST]

Stock agencies instantly reject commercial submissions containing visible or hallucinated trademark names (Sony, Canon, Nike, Apple).

Architectural Outcome

Built a post-generation regex inspection layer that scrubs all suggested keywords and descriptions against an extensive commercial brand denylist.

[OFFICIAL TAXONOMY COMPLIANCE]

Shutterstock accepts only exact matches from their 26 official category strings (e.g. 'Animals/Wildlife', 'Nature', 'Signs/Symbols').

Architectural Outcome

Engineered a normalization lookup dictionary that maps arbitrary AI category predictions to verified agency taxonomies.

[ZERO RE-ENCODING ARTIFACTS]

High-resolution camera files must be analyzed without modifying pixel data or stripping EXIF camera metadata.

Architectural Outcome

Separated visual analysis from file modification: Step 1 renames filenames on disk via semantic slugs; Step 2 creates an external CSV mapping without touching raw file streams.

System Architecture & Data Pipeline

A decoupled two-stage CLI pipeline: Step 1 (Visual Renamer) examines image composition via Gemini Vision to generate standardized snake_case slugs; Step 2 (Metadata Generator) batches images, queries the model with structured prompt contracts, validates brand safety, and outputs ingest-ready CSVs.

Runtime Dispatch via Virtual Method Table (vtable)
<<Abstract Base>> VehicleInclude/Vehicle.h
- vehicleID: string | model: string | rentalRate: float
- status: VehicleStatus (Available | Rented | Sold)
+ virtual ~Vehicle(); // Mandatory for polymorphic delete
+ virtual calculateCost(int days) = 0;
+ virtual getCategory() const = 0;
EconomyIDs 3000s

Alto, Cultus, Corolla. Standard tiered rental base.

calcCost: days * baseRate
LuxuryIDs 4000s

Audi A6, BMW 7, Land Cruiser. Chauffeur insurance rate.

calcCost: days * baseRate * 1.25
SUVIDs 5000s

Sportage, Tucson, Fortuner. All-terrain security deposit.

calcCost: days * baseRate + terrainFee
VanIDs 6000s

Bolan, Hiace, Coaster. High-capacity commercial rate.

calcCost: days * baseRate (cap > 15)

Dynamic Polymorphism at Runtime: The orchestrator holds a single container std::vector<Vehicle*> fleet. When executing reservations or computing quotes, method calls to v->calculateCost(days) dynamically dispatch to the concrete subclass implementation through each instance's vtable pointer.

Subsystem Decomposition

AI Visual Renamer (Step 1)

image-file-rename-script

Scans raw directory folders and generates concise, descriptive, SEO-rich file names based on visual geometry.

Impl: Inspects primary subject matter and lighting conditions (e.g., transforming IMG_4012.JPG into yellow_paper_wasp_macro.jpg).

API Key Pooling & Rate Handler

gemini_client.py

Rotates through an array of API credentials to maximize throughput while respecting Google AI Studio rate limits.

Impl: Detects HTTP 429 quota exhaustion and rotates to the next healthy key with jittered exponential backoff.

Multimodal Metadata Generator (Step 2)

stockflow-script

Produces descriptive titles, 20–40 ranked keywords, and dual category classifications per photo.

Impl: Sends batched 5-image payloads with strict JSON schema instructions to minimize latency and token overhead.

Validation & Ingestion Exporter

validation.py & CSV Writer

Enforces agency guidelines, scrubs brand names, and generates the final CSV file.

Impl: Formats output according to Shutterstock Contributor specifications: Filename, Description, Keywords, Categories, Editorial.

The Hard Part: Multimodal Hallucinations & Commercial Trademark Violations

Preventing AI vision models from inventing brand names or violating strict marketplace metadata schemas.

Stock agencies impose severe penalties—including account bans—for commercial submissions containing copyrighted brand names in keywords or descriptions. Multimodal vision models frequently over-attribute equipment names (like guessing 'Canon EOS' or 'Sony G Master') based on macro depth-of-field cues, even when no logos are present.

Because foundation models associate high-end photography terms with camera brands in their training weights, they instinctively inject manufacturer names into keyword lists, instantly invalidating the submission.

stockflow-script/validation.py
python
# Multi-tier brand denylist validation
BRAND_DENYLIST = load_brand_denylist() # Sony, Canon, Nikon, Apple, Nike...

def validate_row(filename: str, description: str, keywords: list[str]) -> tuple[bool, list[str]]:
    warnings = []

    # 1. Inspect Description for trademark mentions
    for brand in BRAND_DENYLIST:
        if re.search(r'\b' + re.escape(brand) + r'\b', description, re.IGNORECASE):
            warnings.append(f"Description contains forbidden trademark: '{brand}'")

    # 2. Filter Keyword Array
    clean_keywords = [
        kw for kw in keywords
        if kw.lower() not in BRAND_DENYLIST and len(kw) >= 3
    ]

    # 3. Enforce Agency Quota (20-40 keywords)
    if len(clean_keywords) < 20:
        warnings.append(f"Insufficient keywords ({len(clean_keywords)}) after filtering")

    return len(warnings) == 0, clean_keywords
Strict regex boundary filtering strips hallucinated camera brands from keywords and descriptions before CSV compilation.
The Technical Resolution

We implemented a multi-stage validation layer: first, the prompt explicitly instructs the vision model to describe optical attributes without equipment names; second, post-processing filters every keyword and description against a comprehensive trademark denylist, flagging any violations for manual inspection.

What the System Taught Me

Prompt engineering in production is not about creative phrasing; it is about building defensive type-checking and schema contracts around nondeterministic probabilistic engines.

Batch Ingestion & Shutterstock CSV Compilation

Console execution of Stockflow processing an archive of 24 macro photography captures with key pooling and brand safety checks.

hmsaeed@taxila: ~/projects/vms (x86_64-gcc)
C++17
$python stockflow.py ./macro_archive -o shutterstock_ingest.csv --batch 5
[CONFIG] Loaded 3 active Gemini API keys into credentials pool.
[SCAN] Found 24 supported images (.jpg, .jpeg, .png).
[BATCH 1/5] Processing 5 images with gemini-2.5-flash... OK (2.1s)
[VALIDATION] Row 01: 'oriental_hornet_close_up.jpg' -> 34 keywords verified.
[VALIDATION] Row 02: Scrubbed trademark keyword 'Canon' -> 28 clean keywords.
[BATCH 2/5] Key #1 rate limit approached -> Rotated to Key #2 (0ms delay).
[BATCH 3/5] Processing 5 images... OK (1.9s)
[CATEGORIES] Mapped 'Insects' -> Official Category: 'Animals/Wildlife'.
[EXPORT] 24/24 rows written to 'shutterstock_ingest.csv'.
===========================================================
Success: 100% compliant Shutterstock Bulk Import CSV generated.
$

Engineering Reflection

“AI is most valuable not when it generates synthetic content, but when it removes the mechanical friction that suffocates human creative work.”

As a photographer, spending hours typing descriptive tags and double-checking Shutterstock category dropdowns felt like a waste of human energy. Building Stockflow gave that time back.

The project taught me that the hardest part of building AI software is rarely the model itself—it is the defensive engineering around the model: rate limits, schema validation, brand safety, and edge-case handling.

When an automated pipeline works properly, the technology disappears and you are simply left with the photographs.

Interested in discussing this architecture?

I'm always open to technical dialogue, code reviews, and exploring system constraints.

Start a Technical Conversation→