Skip to content

Commit 96a573f

Browse files
authored
Merge pull request #5 from Pritom14/feat/video_opt
[FEAT] feat: video optimization + model-aware OCR routing, bump to v0.3.0
2 parents f43ce17 + eb29de5 commit 96a573f

13 files changed

Lines changed: 1997 additions & 31 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ build/
1616
.token0_images/
1717
benchmarks/images/real/screenshot_real.png
1818
benchmarks/results/
19+
benchmarks/videos/
1920
*.db-journal
2021
.DS_Store
2122
.idea/

README.md

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Your App → Token0 Proxy → [Analyze → Classify → Route → Transform →
2929
Database (logs every optimization decision + savings)
3030
```
3131

32-
Token0 applies **7 optimizations** automatically:
32+
Token0 applies **9 optimizations** automatically:
3333

3434
### Core Optimizations (Free Tier)
3535

@@ -49,11 +49,15 @@ Token0 applies **7 optimizations** automatically:
4949

5050
**7. Semantic Response Cache** — Cache responses for similar image+prompt pairs using perceptual image hashing. Repeated or similar queries cost 0 tokens. Effective on repetitive workloads (product classification, document processing).
5151

52+
**8. QJL-Compressed Fuzzy Cache** — Similar (not just identical) images hit the cache using Quantized Johnson-Lindenstrauss random projection. Compresses 256-bit perceptual hashes to 128-bit binary signatures, matches via Hamming distance. Inspired by Google's TurboQuant (arXiv 2504.19874). **62% additional token savings** on image variations in benchmarks — similar product photos, re-scanned documents, and slightly different angles all hit cache.
53+
54+
**9. Video Optimization** — Automatically extract keyframes from video at 1fps, deduplicate similar consecutive frames using QJL perceptual hashing, detect scene changes via pixel-level diff, and run each keyframe through the full image optimization pipeline. A 60-second video at 30fps (1,800 frames) reduces to ~10 keyframes before being sent to the LLM. **13-45% savings on local models; ~83% projected savings on GPT-4o.** Optional CLIP-based query-frame scoring (Layer 2) ranks frames by relevance to the user's prompt.
55+
5256
---
5357

5458
## Benchmarks
5559

56-
We benchmarked Token0 against **4 vision models** on **5 real-world images** (not synthetic — actual photos, receipts, documents, and screenshots), plus cost projections using OpenAI and Anthropic's published token formulas.
60+
We benchmarked Token0 against **7 vision models** on **5 real-world images** (not synthetic — actual photos, receipts, documents, and screenshots) and **3 test videos**, plus cost projections using OpenAI and Anthropic's published token formulas.
5761

5862
### Real-World Image Test Suite
5963

@@ -111,16 +115,55 @@ We benchmarked Token0 against **4 vision models** on **5 real-world images** (no
111115
| Screenshot (2066x766) | 618 | 244 | **60.5%** | **-3,744ms** | OCR route |
112116
| **Total** | **3,027** | **2,243** | **25.9%** | | |
113117

114-
### Summary Across All Models
118+
### Image Benchmark Summary (7 Models)
119+
120+
| Model | Params | Total Direct | Total Token0 | Savings | Notes |
121+
|---|---|---|---|---|---|
122+
| granite3.2-vision | 3B | 129,836 | 60,924 | **53.1%** | High-res image encoder |
123+
| minicpm-v | 8B | 10,877 | 6,276 | **42.3%** | |
124+
| moondream | 1.7B | 16,457 | 10,240 | **37.8%** | |
125+
| llava-llama3 | 8B | 13,365 | 8,486 | **36.5%** | |
126+
| llava:7b | 7B | 13,384 | 8,701 | **35.0%** | |
127+
| gemma3:4b | 4B | 6,380 | 4,798 | **24.8%** | |
128+
| llama3.2-vision | 11B | 665 | 665 | **0%** | Ultra-efficient encoder: passthrough correct, no optimization needed |
129+
130+
> The 0% savings on llama3.2-vision is expected and correct. This model uses ~8-27 tokens per image natively — far below what OCR text extraction would cost. Token0 detects this and correctly skips all lossy optimizations.
131+
132+
### Video Benchmark Results
133+
134+
Test setup: 3 videos (product showcase, document montage, mixed content), naive baseline = all frames at 1fps sent raw, Token0 = frame dedup + scene detection + per-frame image optimization.
135+
136+
| Model | Naive Tokens | Token0 Tokens | Savings |
137+
|---|---|---|---|
138+
| gemma3:4b | 14,706 | 8,081 | **45.0%** |
139+
| llava:7b | 15,731 | 12,845 | **18.3%** |
140+
| llava-llama3 | 15,658 | 12,789 | **18.3%** |
141+
| minicpm-v | 7,428 | 6,447 | **13.2%** |
142+
| moondream | 12,288 | 11,714 | **4.7%** |
115143

116-
| Model | Params | Total Direct | Total Token0 | Savings |
117-
|---|---|---|---|---|
118-
| minicpm-v | 8B | 10,877 | 6,276 | **42.3%** |
119-
| moondream | 1.7B | 16,457 | 10,240 | **37.8%** |
120-
| llava-llama3 | 8B | 13,365 | 8,486 | **36.5%** |
121-
| llava:7b | 7B | 13,384 | 8,701 | **35.0%** |
144+
**Why moondream shows less video savings:** moondream uses a very small frame encoder — its per-frame token cost is already low, so frame dedup has less absolute impact than on higher-token models.
145+
146+
### GPT-4o Video Extrapolation (ballpark)
147+
148+
Using OpenAI's published tile formula (512px tiles, 170 tokens/tile):
149+
150+
| Scenario | Naive | Token0 | Savings |
151+
|---|---|---|---|
152+
| 60s video, 30fps (1,800 frames → 1fps → 60 frames → dedup to ~10) | ~25,500 tokens | ~4,250 tokens | **~83%** |
153+
| Monthly cost at 10K videos/day (GPT-4o $2.50/1M tokens) | $19,125/mo | $3,188/mo | **$15,938/mo saved** |
154+
155+
### Anthropic Video Extrapolation (ballpark)
156+
157+
Using Anthropic's pixel formula (tokens ≈ width × height / 750):
158+
159+
| Scenario | Naive | Token0 | Savings |
160+
|---|---|---|---|
161+
| 60s video, 1fps = 60 frames at 1280×720 | ~73,700 tokens | ~12,300 tokens | **~83%** |
162+
| Monthly cost at 1K videos/day (Claude Sonnet $3/1M tokens) | $6,633/mo | $1,107/mo | **$5,526/mo saved** |
122163

123-
### GPT-4o Cost Projections (v1 vs v2)
164+
> These are linear extrapolations from the token formula + observed dedup ratios (60 frames → ~10 keyframes). Actual savings vary by content type — talking-head video deduplicates more aggressively than action scenes.
165+
166+
### GPT-4o Image Cost Projections (v1 vs v2)
124167

125168
Using OpenAI's published token formulas on real images:
126169

@@ -150,11 +193,13 @@ Using OpenAI's published token formulas on real images:
150193
5. **Prompt-aware detail mode** drops simple queries from 1,105 → 85 tokens (92% savings) on GPT-4o.
151194
6. **Model cascade** routes simple tasks at 16.7x cheaper rates with equivalent quality.
152195
7. **Tile-optimized resize** cuts OpenAI costs by 44% on mid-size images (1280x720) with zero quality loss.
153-
8. **On cloud APIs, total savings reach 98.9%** when all optimizations are combined with model cascading.
196+
8. **On cloud APIs, total image savings reach 98.9%** when all optimizations are combined with model cascading.
197+
9. **Video deduplication collapses 60-frame clips to ~10 keyframes** — 13-45% savings on local models, ~83% projected on GPT-4o.
198+
10. **Model-aware OCR skip is critical** — ultra-efficient encoders like llama3.2-vision use <50 tokens/image; OCR text output would cost more, not less.
154199

155200
### Additional Test Coverage
156201

157-
Token0 includes **103 unit tests** and benchmarks across multiple suites:
202+
Token0 includes **148 unit tests** and benchmarks across multiple suites:
158203

159204
| Suite | Tests | What It Validates |
160205
|---|---|---|
@@ -166,6 +211,8 @@ Token0 includes **103 unit tests** and benchmarks across multiple suites:
166211
| `real` | 5 | Real-world photos, receipts, invoices, screenshots |
167212
| `streaming` | 7 | SSE streaming: format, content, stats, image optimization |
168213
| `litellm` | 10 | LiteLLM hook: passthrough, optimization, OCR, cascade, async |
214+
| `cache` | 23 | QJL fuzzy cache: perceptual hash, JL compression, Hamming distance, fuzzy match |
215+
| `video` | 22 | Frame extraction, QJL dedup, scene detection, CLIP scoring, full pipeline |
169216

170217
---
171218

@@ -234,6 +281,26 @@ response = client.chat.completions.create(
234281
# response.token0.optimizations_applied = ["resize 4000x3000 → 1568x1176", "convert png → jpeg q=85"]
235282
```
236283

284+
### Video Support
285+
286+
Send a video URL or base64-encoded video — Token0 automatically extracts keyframes, deduplicates, and optimizes before forwarding:
287+
288+
```python
289+
response = client.chat.completions.create(
290+
model="gpt-4o",
291+
messages=[{
292+
"role": "user",
293+
"content": [
294+
{"type": "text", "text": "What happens in this video?"},
295+
{"type": "video_url", "video_url": {"url": "data:video/mp4;base64,..."}}
296+
]
297+
}],
298+
extra_headers={"X-Provider-Key": "sk-..."}
299+
)
300+
# 1,800 raw frames → ~10 keyframes → optimized images → LLM
301+
# response.token0.tokens_saved = 21,250 (~83% on GPT-4o)
302+
```
303+
237304
### Streaming Support
238305

239306
Token0 supports `stream=true` — images are optimized before streaming begins, then tokens flow word-by-word via SSE:
@@ -332,12 +399,16 @@ curl http://localhost:8000/v1/usage
332399
pip install token0[dev]
333400
ollama pull moondream
334401

335-
# Run all suites
402+
# Run all image suites
336403
python -m benchmarks.run --model moondream --suite all
337404

338405
# Run only real-world images
339406
python -m benchmarks.run --model llava:7b --suite real
340407

408+
# Run video benchmarks (requires Ollama + real images in benchmarks/images/real/)
409+
python -m benchmarks.bench_video_models
410+
python -m benchmarks.bench_video_models --model llava:7b --model minicpm-v
411+
341412
# Available suites: images, text, multi, turns, tasks, real, all
342413
# Available models: any Ollama vision model
343414
```

benchmarks/bench_fuzzy_cache.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
"""Benchmark: QJL fuzzy cache vs exact-match-only cache.
2+
3+
Demonstrates token savings from fuzzy cache hits on similar images.
4+
Simulates a real workload: same product/document photographed multiple
5+
times with slight variations (lighting, angle, compression artifacts).
6+
7+
Usage:
8+
python -m benchmarks.bench_fuzzy_cache
9+
"""
10+
11+
import asyncio
12+
import time
13+
14+
import numpy as np
15+
from PIL import Image
16+
17+
from token0.optimization.cache import (
18+
_hamming_distance,
19+
_image_hash,
20+
_jl_compress,
21+
clear_fuzzy_index,
22+
get_cached_response,
23+
get_fuzzy_index_size,
24+
make_cache_key,
25+
set_cached_response,
26+
)
27+
from token0.storage.redis import MemoryCache
28+
29+
# Estimated tokens per image (GPT-4o high detail, ~800x600)
30+
TOKENS_PER_IMAGE = 765
31+
COST_PER_TOKEN = 2.50 / 1_000_000 # GPT-4o input price
32+
33+
34+
def _make_base_image(seed: int, width=800, height=600) -> Image.Image:
35+
"""Create a unique base image (simulates a product photo or document)."""
36+
rng = np.random.RandomState(seed=seed)
37+
pixels = rng.randint(0, 256, (height, width, 3), dtype=np.uint8)
38+
return Image.fromarray(pixels)
39+
40+
41+
def _add_variation(base: Image.Image, variation_seed: int, noise_level: int = 15) -> Image.Image:
42+
"""Add slight variation to an image (simulates re-photo, compression, etc.)."""
43+
pixels = np.array(base)
44+
rng = np.random.RandomState(seed=variation_seed)
45+
noise = rng.randint(-noise_level, noise_level + 1, pixels.shape, dtype=np.int16)
46+
noisy = np.clip(pixels.astype(np.int16) + noise, 0, 255).astype(np.uint8)
47+
return Image.fromarray(noisy)
48+
49+
50+
async def run_benchmark():
51+
import token0.storage.redis as redis_mod
52+
53+
redis_mod._memory_cache.clear()
54+
redis_mod.pool = MemoryCache()
55+
clear_fuzzy_index()
56+
57+
print("=" * 80)
58+
print(" QJL Fuzzy Cache Benchmark")
59+
print("=" * 80)
60+
61+
# --- Setup: create base images and variations ---
62+
num_unique_images = 20
63+
variations_per_image = 5 # each base image has 5 slight variations
64+
prompt = "describe this product image"
65+
66+
base_images = [_make_base_image(seed=i) for i in range(num_unique_images)]
67+
variation_images = []
68+
for i, base in enumerate(base_images):
69+
for v in range(variations_per_image):
70+
variation_images.append((i, _add_variation(base, variation_seed=i * 100 + v)))
71+
72+
total_requests = num_unique_images + len(variation_images)
73+
print(f"\n Setup: {num_unique_images} unique images, {variations_per_image} variations each")
74+
print(f" Total requests: {total_requests}")
75+
print(f" Tokens per image (GPT-4o): {TOKENS_PER_IMAGE}")
76+
77+
# --- Benchmark 1: Exact-match only ---
78+
print("\n --- Exact Match Only ---\n")
79+
redis_mod._memory_cache.clear()
80+
clear_fuzzy_index()
81+
82+
exact_hits = 0
83+
exact_misses = 0
84+
start = time.time()
85+
86+
# First pass: cache base images
87+
for i, base in enumerate(base_images):
88+
key = make_cache_key(base, prompt, "gpt-4o")
89+
await set_cached_response(key, {"content": f"response_{i}"})
90+
91+
# Second pass: query with variations (exact match only)
92+
for base_idx, var_img in variation_images:
93+
key = make_cache_key(var_img, prompt, "gpt-4o")
94+
result = await get_cached_response(key, fuzzy=False)
95+
if result:
96+
exact_hits += 1
97+
else:
98+
exact_misses += 1
99+
100+
exact_time = time.time() - start
101+
exact_tokens_used = exact_misses * TOKENS_PER_IMAGE
102+
exact_cost = exact_tokens_used * COST_PER_TOKEN
103+
104+
print(f" Hits: {exact_hits}/{len(variation_images)}")
105+
print(f" Misses: {exact_misses}/{len(variation_images)}")
106+
print(f" Tokens used: {exact_tokens_used:,}")
107+
print(f" Cost: ${exact_cost:.4f}")
108+
print(f" Time: {exact_time * 1000:.1f}ms")
109+
110+
# --- Benchmark 2: Fuzzy match (QJL) ---
111+
print("\n --- QJL Fuzzy Match ---\n")
112+
redis_mod._memory_cache.clear()
113+
clear_fuzzy_index()
114+
115+
fuzzy_hits = 0
116+
fuzzy_misses = 0
117+
start = time.time()
118+
119+
# First pass: cache base images
120+
for i, base in enumerate(base_images):
121+
key = make_cache_key(base, prompt, "gpt-4o")
122+
await set_cached_response(key, {"content": f"response_{i}"})
123+
124+
# Second pass: query with variations (fuzzy match enabled)
125+
for base_idx, var_img in variation_images:
126+
key = make_cache_key(var_img, prompt, "gpt-4o")
127+
result = await get_cached_response(key, fuzzy=True)
128+
if result:
129+
fuzzy_hits += 1
130+
else:
131+
fuzzy_misses += 1
132+
133+
fuzzy_time = time.time() - start
134+
fuzzy_tokens_used = fuzzy_misses * TOKENS_PER_IMAGE
135+
fuzzy_cost = fuzzy_tokens_used * COST_PER_TOKEN
136+
137+
print(f" Hits: {fuzzy_hits}/{len(variation_images)}")
138+
print(f" Misses: {fuzzy_misses}/{len(variation_images)}")
139+
print(f" Tokens used: {fuzzy_tokens_used:,}")
140+
print(f" Cost: ${fuzzy_cost:.4f}")
141+
print(f" Time: {fuzzy_time * 1000:.1f}ms")
142+
print(f" Fuzzy index size: {get_fuzzy_index_size()} entries")
143+
144+
# --- Hamming distance analysis ---
145+
print("\n --- Hamming Distance Analysis ---\n")
146+
distances_similar = []
147+
distances_different = []
148+
149+
for i, base in enumerate(base_images[:5]):
150+
base_hash = _image_hash(base)
151+
base_sig = _jl_compress(base_hash)
152+
153+
# Similar: variations of same base
154+
for v in range(variations_per_image):
155+
var = _add_variation(base, variation_seed=i * 100 + v)
156+
var_hash = _image_hash(var)
157+
var_sig = _jl_compress(var_hash)
158+
distances_similar.append(_hamming_distance(base_sig, var_sig))
159+
160+
# Different: other base images
161+
for j in range(5):
162+
if i == j:
163+
continue
164+
other_hash = _image_hash(base_images[j])
165+
other_sig = _jl_compress(other_hash)
166+
distances_different.append(_hamming_distance(base_sig, other_sig))
167+
168+
print(
169+
f" Similar images: avg={np.mean(distances_similar):.1f}, "
170+
f"min={min(distances_similar)}, max={max(distances_similar)}"
171+
)
172+
print(
173+
f" Different images: avg={np.mean(distances_different):.1f}, "
174+
f"min={min(distances_different)}, max={max(distances_different)}"
175+
)
176+
177+
# --- Summary ---
178+
print(f"\n {'=' * 70}")
179+
print(" SUMMARY")
180+
print(f" {'=' * 70}")
181+
print(f" {'':30s} {'Exact':>12s} {'Fuzzy (QJL)':>12s} {'Improvement':>12s}")
182+
print(f" {'-' * 30} {'-' * 12} {'-' * 12} {'-' * 12}")
183+
print(
184+
f" {'Cache hits':30s} {exact_hits:>12d} {fuzzy_hits:>12d} "
185+
f"{'+' + str(fuzzy_hits - exact_hits):>12s}"
186+
)
187+
print(
188+
f" {'Cache misses':30s} {exact_misses:>12d} {fuzzy_misses:>12d} "
189+
f"{exact_misses - fuzzy_misses:>12d}"
190+
)
191+
print(
192+
f" {'Tokens used':30s} {exact_tokens_used:>12,} {fuzzy_tokens_used:>12,} "
193+
f"{exact_tokens_used - fuzzy_tokens_used:>12,}"
194+
)
195+
196+
if exact_tokens_used > 0:
197+
savings_pct = (exact_tokens_used - fuzzy_tokens_used) / exact_tokens_used * 100
198+
print(f" {'Token savings':30s} {'':>12s} {'':>12s} {savings_pct:>11.1f}%")
199+
200+
print(
201+
f" {'Cost (GPT-4o)':30s} ${exact_cost:>11.4f} ${fuzzy_cost:>11.4f} "
202+
f"${exact_cost - fuzzy_cost:>11.4f}"
203+
)
204+
205+
# Scale projections
206+
print("\n At scale (100K images/day, 20% are variations):")
207+
daily_variations = 20_000
208+
exact_miss_rate = exact_misses / len(variation_images)
209+
fuzzy_miss_rate = fuzzy_misses / len(variation_images)
210+
daily_exact_tokens = daily_variations * TOKENS_PER_IMAGE * exact_miss_rate
211+
daily_fuzzy_tokens = daily_variations * TOKENS_PER_IMAGE * fuzzy_miss_rate
212+
monthly_exact = daily_exact_tokens * 30 * COST_PER_TOKEN
213+
monthly_fuzzy = daily_fuzzy_tokens * 30 * COST_PER_TOKEN
214+
print(f" Exact-only monthly cost: ${monthly_exact:,.2f}")
215+
print(f" Fuzzy cache monthly cost: ${monthly_fuzzy:,.2f}")
216+
print(f" Monthly savings: ${monthly_exact - monthly_fuzzy:,.2f}")
217+
print(f" {'=' * 70}")
218+
219+
# Memory overhead
220+
sig_bytes = get_fuzzy_index_size() * 16 # 16 bytes per signature
221+
key_bytes = get_fuzzy_index_size() * 80 # ~80 bytes per cache key string
222+
print(
223+
f"\n Memory overhead: {(sig_bytes + key_bytes) / 1024:.1f} KB "
224+
f"for {get_fuzzy_index_size()} entries "
225+
f"({sig_bytes} bytes signatures + {key_bytes} bytes keys)"
226+
)
227+
print(f" At 1M entries: ~{(1_000_000 * 96) / 1024 / 1024:.1f} MB")
228+
229+
230+
if __name__ == "__main__":
231+
asyncio.run(run_benchmark())

0 commit comments

Comments
 (0)