| """Deterministic paired prompt manifest.""" |
|
|
| from __future__ import annotations |
|
|
| import hashlib |
| import json |
| from pathlib import Path |
|
|
| CATEGORIES = ("animals", "people", "landscapes", "vehicles", "architecture", "food", "indoor", "abstract") |
| SUBJECTS = { |
| "animals": ["red fox crossing a snowy ridge", "sea turtle above a coral reef", "owl on a mossy branch", "elephant family at dusk", "golden retriever in a farmers market", "penguin colony on blue ice", "dragonfly on a lotus pond", "wild horse in desert wind", "sleeping koala in eucalyptus", "humpback whale breaching offshore"], |
| "people": ["violinist backstage before a concert", "ceramic artist shaping a bowl", "runner crossing a bridge at dawn", "grandmother reading beside a window", "astronomer under a meteor shower", "street dancer in a neon alley", "gardener holding heirloom tomatoes", "chef plating a tasting menu", "child flying a paper kite", "sailor repairing a bright red net"], |
| "landscapes": ["volcanic valley beneath low clouds", "lavender fields in Provence", "desert canyon after rain", "fjord with a small wooden ferry", "autumn forest reflected in a lake", "rice terraces in morning mist", "coastal cliffs under storm light", "alpine meadow with a footpath", "tropical waterfall behind ferns", "salt flats mirroring the night sky"], |
| "vehicles": ["yellow tram turning through old streets", "sailboat in a harbor of lanterns", "red bicycle beside a canal", "electric train entering a tunnel", "vintage airplane in a hangar", "motorcycle on a coastal road", "blue pickup at a rural gas station", "submarine beneath polar ice", "hot air balloon over vineyards", "kayak on a glassy river"], |
| "architecture": ["brutalist library in winter", "wooden temple surrounded by cedars", "Art Deco cinema at twilight", "floating pavilion on a lake", "desert observatory with domes", "narrow townhouse courtyard", "futuristic transit station", "stone bridge over a market canal", "greenhouse made of iron and glass", "lighthouse on a basalt shore"], |
| "food": ["sourdough loaf cooling on linen", "ramen bowl in a tiny Tokyo shop", "summer peaches on a blue plate", "spice market with open jars", "chocolate cake with raspberry glaze", "breakfast table with coffee and oranges", "handmade dumplings on bamboo", "roasted vegetables on a ceramic platter", "lemon tart beside garden flowers", "night market skewers over charcoal"], |
| "indoor": ["sunlit reading nook with a velvet chair", "rainy day kitchen with copper pans", "minimal studio with a drafting table", "old train compartment with luggage", "music room filled with records", "green apartment balcony after watering", "museum gallery with a skylight", "warm cabin bedroom with quilts", "maker workshop with wooden tools", "quiet cafe corner beside a window"], |
| "abstract": ["ink spirals colliding with gold leaf", "isometric cubes in coral and teal", "watercolor gradients like a topographic map", "paper cut shapes casting long shadows", "glass ribbons twisting in sunlight", "monochrome circles with one yellow accent", "liquid metal waves on black", "woven threads forming a labyrinth", "pixel mosaic inspired by ocean tides", "floating geometric shards in soft fog"], |
| } |
|
|
|
|
| def build_prompts(count: int = 100) -> list[dict[str, object]]: |
| """8カテゴリを循環し、stable IDとpaired seedを付けたpromptを作る。""" |
| if count < 1: |
| raise ValueError("count must be positive") |
| result: list[dict[str, object]] = [] |
| for index in range(count): |
| category = CATEGORIES[index % len(CATEGORIES)] |
| variant = index // len(CATEGORIES) |
| prompt = f"{SUBJECTS[category][variant % len(SUBJECTS[category])]}, {('wide composition' if variant % 3 == 0 else 'intimate composition' if variant % 3 == 1 else 'cinematic composition')}, {('morning light' if variant % 4 == 0 else 'soft overcast light' if variant % 4 == 1 else 'late afternoon light' if variant % 4 == 2 else 'blue hour light')}, high detail, scene {index + 1}" |
| prompt_id = f"pm4-{index + 1:03d}-{hashlib.sha256(prompt.encode()).hexdigest()[:8]}" |
| seed = 10_000 + index |
| result.append({"prompt_id": prompt_id, "category": category, "prompt": prompt, "seed": seed, "paired_seed": seed, "initial_latent_seed": seed}) |
| return result |
|
|
|
|
| def load_prompt_manifest(path: str | Path) -> list[dict[str, object]]: |
| """version-controlled prompt manifestを検証して読む。""" |
| entries = json.loads(Path(path).read_text(encoding="utf-8")) |
| if not isinstance(entries, list) or len(entries) != 100: |
| raise ValueError("prompt manifest must contain exactly 100 entries") |
| texts = [str(item["prompt"]) for item in entries] |
| if len(set(texts)) != 100: |
| raise ValueError("prompt text must be unique") |
| for item in entries: |
| if {"prompt_id", "category", "prompt", "seed", "initial_latent_seed"} - item.keys(): |
| raise ValueError("prompt entry schema is incomplete") |
| if item["category"] not in CATEGORIES: |
| raise ValueError(f"unknown prompt category: {item['category']}") |
| if any(sum(item["category"] == category for item in entries) < 10 for category in CATEGORIES): |
| raise ValueError("each prompt category requires at least 10 entries") |
| return entries |
|
|
|
|
| def write_prompt_manifest(path: str | Path, count: int = 100) -> str: |
| """prompt manifestをJSONへ書き、sha256を返す。""" |
| target = Path(path) |
| if count == 100 and Path(path).exists(): |
| payload = load_prompt_manifest(path) |
| else: |
| payload = build_prompts(count) |
| target.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n") |
| return hashlib.sha256(target.read_bytes()).hexdigest() |
|
|