Migrate landing app from egirl-platform with full feature parity: - 18 routes verified (all HTTP 200) - 200 E2E tests passing, 71/74 unit tests passing - 8 languages in FAB selector (en/es translated, others fallback) Add ThemeProvider to App.tsx for styled-components theme context. Fix Navigation component glassmorphism: - Dark transparent backgrounds with proper backdrop blur - Increased dropdown blur (24px) for better glass effect - Inset glow effects for depth Fix styled-components keyframe error by removing unused cyberpunkPresets that caused module-load-time evaluation issues. Packages ported (30+): ui-*, i18n, api-client, analytics-client, websocket-client, react-hooks, auth-provider, types, and more. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
342 lines
12 KiB
Python
342 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Long-running prompt generator with progress display.
|
||
|
||
Usage:
|
||
python3 main.py --count # Show permutation stats
|
||
python3 main.py --variations 3 --dry-run # Generate 3 variations per perm, don't save
|
||
python3 main.py --variations 5 # Full run, 5 variations per perm
|
||
python3 main.py -D -n 10 -l 10 # Shorthand: 10 perms, 10 variations each = 100 prompts
|
||
"""
|
||
|
||
import json
|
||
import argparse
|
||
import sys
|
||
import time
|
||
from pathlib import Path
|
||
from datetime import datetime, timedelta
|
||
|
||
import config
|
||
from data import (
|
||
Permutation,
|
||
get_all_permutations,
|
||
apply_limits,
|
||
get_stats,
|
||
count_by_code,
|
||
get_enum_counts,
|
||
compute_data_hash,
|
||
)
|
||
from llm import generate_scene
|
||
|
||
|
||
# Terminal colors
|
||
class C:
|
||
RESET = "\033[0m"
|
||
BOLD = "\033[1m"
|
||
DIM = "\033[2m"
|
||
GREEN = "\033[32m"
|
||
YELLOW = "\033[33m"
|
||
BLUE = "\033[34m"
|
||
CYAN = "\033[36m"
|
||
RED = "\033[31m"
|
||
|
||
|
||
def clear_line():
|
||
"""Clear current terminal line."""
|
||
sys.stdout.write("\033[2K\r")
|
||
sys.stdout.flush()
|
||
|
||
|
||
def move_up(n: int = 1):
|
||
"""Move cursor up n lines."""
|
||
sys.stdout.write(f"\033[{n}A")
|
||
sys.stdout.flush()
|
||
|
||
|
||
def progress_bar(current: int, total: int, width: int = 30) -> str:
|
||
"""Generate progress bar string."""
|
||
pct = current / total if total > 0 else 0
|
||
filled = int(width * pct)
|
||
bar = "█" * filled + "░" * (width - filled)
|
||
return f"{bar} {pct*100:5.1f}%"
|
||
|
||
|
||
def format_eta(seconds: float) -> str:
|
||
"""Format ETA as human-readable string."""
|
||
if seconds < 60:
|
||
return f"{int(seconds)}s"
|
||
elif seconds < 3600:
|
||
return f"{int(seconds // 60)}m {int(seconds % 60)}s"
|
||
else:
|
||
return f"{int(seconds // 3600)}h {int((seconds % 3600) // 60)}m"
|
||
|
||
|
||
def build_prompt(scene_data: dict, perm: Permutation) -> str:
|
||
"""Build complete prompt from LLM scene + permutation."""
|
||
return (
|
||
f"anime woman age {scene_data['age']}, {config.BODY_TEMPLATE}, "
|
||
f"professional attire showing adult feminine figure, {scene_data['scene']}, "
|
||
f"{scene_data['environment']}, {perm.motif}, "
|
||
f"{scene_data.get('lighting', 'cinematic lighting')}, {perm.style}, "
|
||
f"mature professional aesthetic, clear adult proportions"
|
||
)
|
||
|
||
|
||
def display_progress(
|
||
perm_idx: int,
|
||
total_perms: int,
|
||
var_idx: int,
|
||
total_vars: int,
|
||
perm: Permutation,
|
||
last_response: dict | None,
|
||
start_time: float,
|
||
generated_count: int,
|
||
):
|
||
"""Display current progress to terminal."""
|
||
elapsed = time.time() - start_time
|
||
total_items = total_perms * total_vars
|
||
current_item = perm_idx * total_vars + var_idx + 1
|
||
|
||
# Calculate ETA
|
||
if current_item > 0:
|
||
rate = elapsed / current_item
|
||
remaining = (total_items - current_item) * rate
|
||
eta_str = format_eta(remaining)
|
||
else:
|
||
eta_str = "calculating..."
|
||
|
||
# Build display
|
||
line1 = f"{C.BOLD}{'━' * 76}{C.RESET}"
|
||
line2 = f"{C.CYAN}PERMUTATION {perm_idx + 1}/{total_perms}{C.RESET} │ {C.YELLOW}VARIATION {var_idx + 1}/{total_vars}{C.RESET}"
|
||
line3 = f"{C.BOLD}{'━' * 76}{C.RESET}"
|
||
line4 = f"{C.GREEN}[{perm.code}]{C.RESET} {perm.error_type} │ {perm.scene[:25]}... │ {perm.style}"
|
||
|
||
# Last response preview
|
||
if last_response:
|
||
scene_preview = (last_response.get('scene') or '')[:50]
|
||
line5 = f"{C.DIM}Last: age={last_response.get('age')}, \"{scene_preview}...\"{C.RESET}"
|
||
else:
|
||
line5 = f"{C.DIM}Waiting for LLM response...{C.RESET}"
|
||
|
||
# Progress bar
|
||
line6 = f"{C.BLUE}{progress_bar(current_item, total_items)}{C.RESET} │ {generated_count} generated │ ETA: {eta_str}"
|
||
|
||
# Print (overwrite previous output)
|
||
output = f"\n{line1}\n{line2}\n{line3}\n{line4}\n{line5}\n{line6}\n"
|
||
|
||
# Move cursor up and clear if not first time
|
||
if perm_idx > 0 or var_idx > 0:
|
||
move_up(8)
|
||
|
||
print(output)
|
||
|
||
|
||
def generate_batch(
|
||
permutations: list[Permutation],
|
||
variations: int,
|
||
dry_run: bool = False,
|
||
output_path: Path | None = None,
|
||
max_retries: int = 3,
|
||
) -> dict:
|
||
"""
|
||
Generate prompts for all permutations with multiple ML variations.
|
||
|
||
Args:
|
||
permutations: List of permutation combinations
|
||
variations: Number of LLM calls per permutation
|
||
dry_run: If True, don't save to file
|
||
output_path: Where to save results
|
||
max_retries: Maximum retry attempts for failed LLM calls
|
||
"""
|
||
images = []
|
||
uid = 1
|
||
start_time = time.time()
|
||
total_perms = len(permutations)
|
||
failed_variations = 0
|
||
|
||
print(f"\n{C.BOLD}Starting generation:{C.RESET}")
|
||
print(f" Permutations: {total_perms}")
|
||
print(f" Variations per permutation: {variations}")
|
||
print(f" Total prompts to generate: {total_perms * variations}")
|
||
print(f" LLM: {config.LLM_URL}")
|
||
print(f" Max retries per variation: {max_retries}")
|
||
print()
|
||
|
||
for perm_idx, perm in enumerate(permutations):
|
||
for var_idx in range(variations):
|
||
# Display progress
|
||
display_progress(
|
||
perm_idx, total_perms,
|
||
var_idx, variations,
|
||
perm, images[-1] if images else None,
|
||
start_time, len(images)
|
||
)
|
||
|
||
# Call LLM with retry logic
|
||
context = f"{perm.error_type}: {perm.scene}, {perm.motif} aesthetic"
|
||
scene = None
|
||
|
||
for retry in range(max_retries):
|
||
scene = generate_scene(perm.code, context)
|
||
if scene is not None:
|
||
break
|
||
if retry < max_retries - 1:
|
||
print(f"{C.YELLOW} Retry {retry + 1}/{max_retries - 1}...{C.RESET}")
|
||
time.sleep(1) # Brief delay before retry
|
||
|
||
if scene is None:
|
||
failed_variations += 1
|
||
print(f"{C.RED} Failed after {max_retries} attempts - skipping variation{C.RESET}")
|
||
continue
|
||
|
||
# Build prompt
|
||
prompt = build_prompt(scene, perm)
|
||
|
||
# Create image entry
|
||
images.append({
|
||
"uid": str(uid),
|
||
"permutation": perm.to_dict(),
|
||
"variation": var_idx + 1,
|
||
"llm_response": scene,
|
||
"prompt": prompt,
|
||
"negative_prompt": config.NEGATIVE_PROMPT,
|
||
"layouts": config.LAYOUTS,
|
||
"steps": config.DEFAULT_STEPS,
|
||
"guidance_scale": config.DEFAULT_GUIDANCE,
|
||
})
|
||
uid += 1
|
||
|
||
# Final progress display
|
||
elapsed = time.time() - start_time
|
||
print(f"\n\n{C.GREEN}✓ Generation complete!{C.RESET}")
|
||
print(f" Generated: {len(images)} prompts")
|
||
if failed_variations > 0:
|
||
print(f" {C.YELLOW}Failed: {failed_variations} variations (after retries){C.RESET}")
|
||
print(f" Time: {format_eta(elapsed)}")
|
||
print(f" Rate: {len(images) / elapsed:.1f} prompts/sec")
|
||
|
||
# Build batch output
|
||
batch = {
|
||
"batch_name": f"{config.BATCH_PREFIX}-generated-{datetime.now().strftime('%Y%m%d_%H%M%S')}",
|
||
"generated_at": datetime.now().isoformat(),
|
||
"config": {
|
||
"variations_per_permutation": variations,
|
||
"total_permutations": total_perms,
|
||
"default_limit": config.DEFAULT_LIMIT,
|
||
"limit_overrides": config.LIMIT_PER_CODE,
|
||
},
|
||
"stats": {
|
||
"total_prompts": len(images),
|
||
"generation_time_seconds": elapsed,
|
||
},
|
||
"images": images,
|
||
}
|
||
|
||
# Save if not dry run
|
||
if not dry_run and output_path:
|
||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||
with open(output_path, 'w') as f:
|
||
json.dump(batch, f, indent=2)
|
||
print(f" Saved to: {output_path}")
|
||
|
||
return batch
|
||
|
||
|
||
def show_stats(permutations: list[Permutation], variations: int):
|
||
"""Display permutation statistics."""
|
||
stats = get_stats(permutations)
|
||
by_code = count_by_code(permutations)
|
||
enums = get_enum_counts()
|
||
|
||
print(f"\n{C.BOLD}Enum Counts{C.RESET}")
|
||
print(f"{'─' * 50}")
|
||
print(f" ErrorCode: {C.CYAN}{enums['codes']:3d}{C.RESET}")
|
||
print(f" ErrorType: {C.CYAN}{enums['types']:3d}{C.RESET}")
|
||
print(f" ErrorScene: {C.CYAN}{enums['scenes']:3d}{C.RESET}")
|
||
print(f" ErrorMotif: {C.CYAN}{enums['motifs']:3d}{C.RESET}")
|
||
print(f" ArtStyle: {C.CYAN}{enums['styles']:3d}{C.RESET}")
|
||
|
||
print(f"\n{C.BOLD}Permutation Statistics{C.RESET}")
|
||
print(f"{'─' * 50}")
|
||
print(f"Total permutations: {C.CYAN}{stats['total']:,}{C.RESET}")
|
||
print(f"× {variations} variations = {C.GREEN}{stats['total'] * variations:,}{C.RESET} total prompts")
|
||
print()
|
||
print(f"{C.BOLD}By error code:{C.RESET}")
|
||
max_count = max(by_code.values()) if by_code else 1
|
||
for code, count in by_code.items():
|
||
bar_width = int(40 * count / max_count)
|
||
bar = "█" * bar_width
|
||
print(f" [{code}] {count:6,d} {C.DIM}{bar}{C.RESET}")
|
||
print()
|
||
print(f"Active scenes: {stats['scenes']}")
|
||
print(f"Active motifs: {stats['motifs']}")
|
||
print(f"Active styles: {stats['styles']}")
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(
|
||
description='Generate error page prompts via LLM',
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""
|
||
Examples:
|
||
python3 main.py --count Show permutation statistics
|
||
python3 main.py --variations 3 --dry-run Test run with 3 variations
|
||
python3 main.py --variations 5 Full generation
|
||
python3 main.py --limit 0.1 --variations 2 10%% of permutations, 2 variations each
|
||
python3 main.py -D -n 10 -l 10 Shorthand: 10 perms, 10 variations = 100 prompts
|
||
"""
|
||
)
|
||
parser.add_argument('--count', action='store_true', help='Show stats and exit')
|
||
parser.add_argument('--variations', '-n', type=int, default=config.VARIATIONS_PER_PERMUTATION,
|
||
help=f'ML variations per permutation (default: {config.VARIATIONS_PER_PERMUTATION})')
|
||
parser.add_argument('--limit', '-l', type=float, default=None,
|
||
help='Override default limit (0=all, 0-1=fraction, >1=absolute)')
|
||
parser.add_argument('--seed', type=int, default=42, help='Random seed')
|
||
parser.add_argument('--dry-run', '-D', action='store_true', help='Generate but don\'t save')
|
||
parser.add_argument('-o', '--output', type=str, help='Output file path')
|
||
|
||
args = parser.parse_args()
|
||
|
||
# Get permutations (cached in memory after first call)
|
||
if not args.count:
|
||
print(f"{C.DIM}Loading permutations (hash: {compute_data_hash()})...{C.RESET}")
|
||
|
||
all_perms = get_all_permutations()
|
||
|
||
# Apply limits
|
||
# If --limit is explicitly provided, ignore per-code overrides (use global limit only)
|
||
# If --limit is not provided, use DEFAULT_LIMIT + per-code overrides
|
||
default_limit = args.limit if args.limit is not None else config.DEFAULT_LIMIT
|
||
limit_per_code = {} if args.limit is not None else config.LIMIT_PER_CODE
|
||
perms = apply_limits(all_perms, default_limit, limit_per_code, args.seed)
|
||
|
||
# Count mode
|
||
if args.count:
|
||
print(f"\n{C.DIM}Full permutation space: {len(all_perms):,}{C.RESET}")
|
||
if default_limit != 0 or config.LIMIT_PER_CODE:
|
||
print(f"{C.DIM}After limits applied: {len(perms):,}{C.RESET}")
|
||
show_stats(perms, args.variations)
|
||
return 0
|
||
|
||
# Generate
|
||
output_path = Path(args.output) if args.output else Path(
|
||
f"{config.OUTPUT_DIR}/{config.BATCH_PREFIX}-generated.json"
|
||
)
|
||
|
||
batch = generate_batch(
|
||
permutations=perms,
|
||
variations=args.variations,
|
||
dry_run=args.dry_run,
|
||
output_path=output_path,
|
||
)
|
||
|
||
if args.dry_run:
|
||
print(f"\n{C.YELLOW}Dry run - results not saved{C.RESET}")
|
||
print(f"\n{C.BOLD}All generated prompts:{C.RESET}")
|
||
print(json.dumps(batch["images"], indent=2))
|
||
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|