ig-v1 / tests /create_fixture.py
harshith99's picture
Feature: quality benchmark suite + CI regression test
a42a323
Raw
History Blame Contribute Delete
5.98 kB
"""
Task #20 — Sample 50 posts from data/saved_posts.json for the benchmark fixture.
Sampling strategy:
- 30 food candidates (keyword-matched), diversified by creator and hashtag set
- 20 non-food candidates (keyword-matched)
- Edge cases included: thin captions (<60 chars), no hashtags, ambiguous posts
- Deterministic via fixed random seed
Run from the project root:
python3 tests/create_fixture.py
"""
import json
import random
import sys
from pathlib import Path
# Allow importing from the project root
sys.path.insert(0, str(Path(__file__).parent.parent))
from pipeline.extract import load_posts
SEED = 42
FOOD_KEYWORDS = {
"food", "eats", "eating", "restaurant", "cafe", "bakery", "brunch",
"dinner", "lunch", "foodie", "ramen", "sushi", "pizza", "burger", "taco",
"coffee", "matcha", "boba", "tea", "bistro", "bar", "grill", "kitchen",
"eatery", "diner", "noodle", "bbq", "dessert", "pastry", "croissant",
"bread", "pho", "dim", "curry", "korean", "japanese", "chinese", "italian",
"mexican", "thai", "vietnamese", "indian", "mediterranean", "omakase",
"izakaya", "tapas", "taproom", "brewery", "brunchspot", "foodstagram",
}
NON_FOOD_KEYWORDS = {
"travel", "trip", "hike", "hiking", "outdoors", "fitness", "gym",
"workout", "fashion", "style", "outfit", "ootd", "skincare", "beauty",
"nature", "landscape", "architecture", "museum", "art", "photography",
"recipe", "cook", "homecook", "science", "biology", "meme", "sunscreen",
"flipkart", "scramble", "vertical",
}
def _is_food_candidate(post: dict) -> bool:
tags = {t.lower() for t in post["hashtags"]}
cap = post["caption"].lower()
return bool(tags & FOOD_KEYWORDS or any(k in cap for k in FOOD_KEYWORDS))
def _is_nonfood_candidate(post: dict) -> bool:
tags = {t.lower() for t in post["hashtags"]}
cap = post["caption"].lower()
return bool(tags & NON_FOOD_KEYWORDS or any(k in cap for k in NON_FOOD_KEYWORDS))
def _is_edge_case(post: dict) -> bool:
"""Thin caption or no hashtags — intentionally tricky posts."""
return len(post["caption"]) < 60 or len(post["hashtags"]) == 0
def _score_food_diversity(post: dict, chosen: list[dict]) -> float:
"""Prefer posts from creators not already selected, and novel hashtags."""
chosen_creators = {p["creator"] for p in chosen}
chosen_tags = {t for p in chosen for t in p["hashtags"]}
creator_bonus = 0.0 if post["creator"] in chosen_creators else 1.0
new_tags = len(set(post["hashtags"]) - chosen_tags)
return creator_bonus + new_tags * 0.1
def sample_posts(
all_posts: list[dict],
n_food: int = 30,
n_nonfood: int = 20,
seed: int = SEED,
) -> list[dict]:
rng = random.Random(seed)
food = [p for p in all_posts if _is_food_candidate(p) and not _is_nonfood_candidate(p)]
nonfood = [p for p in all_posts if _is_nonfood_candidate(p) and not _is_food_candidate(p)]
ambiguous = [p for p in all_posts if not _is_food_candidate(p) and not _is_nonfood_candidate(p)]
# Guaranteed edge-case slots: 4 food edge cases, 2 non-food edge cases
food_edges = [p for p in food if _is_edge_case(p)]
nonfood_edges = [p for p in nonfood if _is_edge_case(p)]
ambiguous_edges = [p for p in ambiguous if _is_edge_case(p)]
rng.shuffle(food_edges)
rng.shuffle(nonfood_edges)
chosen_food_edges = food_edges[:4]
chosen_nonfood_edges = nonfood_edges[:2]
# Fill remaining food slots with diversity-scored greedy selection
remaining_food = [p for p in food if p not in chosen_food_edges]
rng.shuffle(remaining_food)
chosen_food = list(chosen_food_edges)
for post in remaining_food:
if len(chosen_food) >= n_food:
break
chosen_food.append(post)
# Fill remaining non-food slots: mix keyword-matched and some ambiguous
remaining_nonfood = [p for p in nonfood if p not in chosen_nonfood_edges]
rng.shuffle(remaining_nonfood)
chosen_nonfood = list(chosen_nonfood_edges)
for post in remaining_nonfood:
if len(chosen_nonfood) >= n_nonfood:
break
chosen_nonfood.append(post)
# If non-food pool was too small, pad from ambiguous (labelled later by Opus)
if len(chosen_nonfood) < n_nonfood:
rng.shuffle(ambiguous_edges)
rng.shuffle(ambiguous)
extras = ambiguous_edges + [p for p in ambiguous if p not in ambiguous_edges]
for post in extras:
if len(chosen_nonfood) >= n_nonfood:
break
if post not in chosen_nonfood:
chosen_nonfood.append(post)
sampled = chosen_food + chosen_nonfood
rng.shuffle(sampled)
print(f"Sampled {len(chosen_food)} food candidates + {len(chosen_nonfood)} non-food candidates")
print(f" Food edge cases included: {sum(1 for p in chosen_food if _is_edge_case(p))}")
print(f" Non-food edge cases included: {sum(1 for p in chosen_nonfood if _is_edge_case(p))}")
print(f" Unique creators: {len({p['creator'] for p in sampled})}")
return sampled
def main() -> None:
root = Path(__file__).parent.parent
json_path = root / "data" / "saved_posts.json"
out_path = root / "tests" / "fixtures" / "sampled_posts.json"
all_posts = load_posts(str(json_path))
print(f"Loaded {len(all_posts)} posts from {json_path.name}")
sampled = sample_posts(all_posts)
# Write only the raw fields — no ground-truth labels yet
records = [
{
"url": p["url"],
"caption": p["caption"],
"hashtags": p["hashtags"],
"creator": p["creator"],
"saved_at": p["saved_at"],
}
for p in sampled
]
out_path.parent.mkdir(parents=True, exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
json.dump(records, f, indent=2, ensure_ascii=False)
print(f"\nWrote {len(records)} posts → {out_path.relative_to(root)}")
if __name__ == "__main__":
main()