feature updates
This commit is contained in:
BIN
tools/__pycache__/ability_processor.cpython-312.pyc
Normal file
BIN
tools/__pycache__/ability_processor.cpython-312.pyc
Normal file
Binary file not shown.
4628
tools/ability_processor.py
Normal file
4628
tools/ability_processor.py
Normal file
File diff suppressed because it is too large
Load Diff
372
tools/ability_validator.py
Normal file
372
tools/ability_validator.py
Normal file
@@ -0,0 +1,372 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ability Validator - Reviews processed abilities and identifies issues for correction.
|
||||
|
||||
This tool helps ensure all abilities are correctly parsed before shipping to players.
|
||||
Run this after ability_processor.py to review and fix any problems.
|
||||
|
||||
Usage:
|
||||
python tools/ability_validator.py # Full validation report
|
||||
python tools/ability_validator.py --unparsed # Show only unparsed abilities
|
||||
python tools/ability_validator.py --low-confidence # Show low confidence parses
|
||||
python tools/ability_validator.py --card 1-001H # Validate specific card
|
||||
python tools/ability_validator.py --effect DAMAGE # Show all DAMAGE effects
|
||||
python tools/ability_validator.py --summary # Quick summary only
|
||||
python tools/ability_validator.py --export issues.json # Export issues for review
|
||||
"""
|
||||
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
ABILITIES_FILE = Path(__file__).parent.parent / "data" / "abilities_processed.json"
|
||||
CARDS_FILE = Path(__file__).parent.parent / "data" / "cards.json"
|
||||
|
||||
|
||||
def load_abilities() -> dict:
|
||||
"""Load processed abilities file."""
|
||||
if not ABILITIES_FILE.exists():
|
||||
print(f"ERROR: {ABILITIES_FILE} not found!")
|
||||
print("Run: python tools/ability_processor.py first")
|
||||
return {}
|
||||
|
||||
with open(ABILITIES_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_cards() -> dict:
|
||||
"""Load cards file for reference."""
|
||||
if not CARDS_FILE.exists():
|
||||
return {}
|
||||
|
||||
with open(CARDS_FILE, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
cards = data.get('cards', data) if isinstance(data, dict) else data
|
||||
return {card['id']: card for card in cards}
|
||||
|
||||
|
||||
def get_issues(abilities_data: dict) -> dict:
|
||||
"""Analyze abilities and categorize issues."""
|
||||
issues = {
|
||||
'unparsed': [], # Completely failed to parse
|
||||
'low_confidence': [], # Parsed but low confidence
|
||||
'unknown_effects': [], # Has UNKNOWN effect type
|
||||
'missing_targets': [], # Effect needs target but none specified
|
||||
'complex_conditions': [], # Has conditions we can't fully parse
|
||||
}
|
||||
|
||||
abilities = abilities_data.get('abilities', {})
|
||||
|
||||
for card_id, card_abilities in abilities.items():
|
||||
for ability in card_abilities:
|
||||
parsed = ability.get('parsed', {})
|
||||
confidence = ability.get('parse_confidence', 'LOW')
|
||||
original = ability.get('original', {})
|
||||
|
||||
# Check for unparsed
|
||||
if not parsed or parsed.get('type') == 'UNPARSED':
|
||||
issues['unparsed'].append({
|
||||
'card_id': card_id,
|
||||
'ability_index': ability.get('ability_index', 0),
|
||||
'original': original,
|
||||
'reason': 'Failed to parse ability type'
|
||||
})
|
||||
continue
|
||||
|
||||
# Check confidence level
|
||||
if confidence == 'LOW':
|
||||
issues['low_confidence'].append({
|
||||
'card_id': card_id,
|
||||
'ability_index': ability.get('ability_index', 0),
|
||||
'original': original,
|
||||
'parsed': parsed,
|
||||
'reason': 'Low confidence parse - may be incorrect'
|
||||
})
|
||||
|
||||
# Check for UNKNOWN effects
|
||||
for effect in parsed.get('effects', []):
|
||||
if effect.get('type') == 'UNKNOWN':
|
||||
issues['unknown_effects'].append({
|
||||
'card_id': card_id,
|
||||
'ability_index': ability.get('ability_index', 0),
|
||||
'original': original,
|
||||
'effect': effect,
|
||||
'reason': f"Unknown effect: {effect.get('raw', 'no text')[:80]}"
|
||||
})
|
||||
|
||||
# Check for missing targets on effects that need them
|
||||
effect_type = effect.get('type', '')
|
||||
needs_target = effect_type in ['DAMAGE', 'BREAK', 'DULL', 'ACTIVATE', 'RETURN', 'POWER_MOD']
|
||||
has_target = effect.get('target', {})
|
||||
|
||||
if needs_target and not has_target:
|
||||
issues['missing_targets'].append({
|
||||
'card_id': card_id,
|
||||
'ability_index': ability.get('ability_index', 0),
|
||||
'effect_type': effect_type,
|
||||
'original': original,
|
||||
'reason': f'{effect_type} effect missing target specification'
|
||||
})
|
||||
|
||||
# Check for complex conditions
|
||||
trigger = parsed.get('trigger', {})
|
||||
if trigger.get('condition'):
|
||||
issues['complex_conditions'].append({
|
||||
'card_id': card_id,
|
||||
'ability_index': ability.get('ability_index', 0),
|
||||
'original': original,
|
||||
'condition': trigger.get('condition'),
|
||||
'reason': 'Has conditional trigger - verify correctness'
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def print_summary(abilities_data: dict, issues: dict) -> None:
|
||||
"""Print a summary of the validation results."""
|
||||
stats = abilities_data.get('statistics', {})
|
||||
|
||||
print("=" * 70)
|
||||
print("ABILITY VALIDATION SUMMARY")
|
||||
print("=" * 70)
|
||||
print(f"Generated: {abilities_data.get('generated_at', 'unknown')}")
|
||||
print(f"Version: {abilities_data.get('version', 'unknown')}")
|
||||
print()
|
||||
print(f"Total Cards: {stats.get('total_cards', 0):,}")
|
||||
print(f"Total Abilities: {stats.get('total_abilities', 0):,}")
|
||||
print()
|
||||
print("Parse Confidence:")
|
||||
print(f" HIGH: {stats.get('parsed_high', 0):,} ({100*stats.get('parsed_high', 0)/max(1, stats.get('total_abilities', 1)):.1f}%)")
|
||||
print(f" MEDIUM: {stats.get('parsed_medium', 0):,} ({100*stats.get('parsed_medium', 0)/max(1, stats.get('total_abilities', 1)):.1f}%)")
|
||||
print(f" LOW: {stats.get('parsed_low', 0):,} ({100*stats.get('parsed_low', 0)/max(1, stats.get('total_abilities', 1)):.1f}%)")
|
||||
print()
|
||||
print("Issues Found:")
|
||||
print(f" Unparsed abilities: {len(issues['unparsed']):,}")
|
||||
print(f" Low confidence: {len(issues['low_confidence']):,}")
|
||||
print(f" Unknown effects: {len(issues['unknown_effects']):,}")
|
||||
print(f" Missing targets: {len(issues['missing_targets']):,}")
|
||||
print(f" Complex conditions: {len(issues['complex_conditions']):,}")
|
||||
print()
|
||||
|
||||
total_issues = sum(len(v) for v in issues.values())
|
||||
if total_issues == 0:
|
||||
print("✓ No issues found! Abilities are ready for shipping.")
|
||||
else:
|
||||
print(f"⚠ {total_issues} total issues to review")
|
||||
print()
|
||||
print("Run with --unparsed, --low-confidence, or --effect to see details")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
def print_issues(issues: list, title: str, cards: dict, limit: int = 50) -> None:
|
||||
"""Print a list of issues with details."""
|
||||
if not issues:
|
||||
print(f"\n{title}: None found ✓")
|
||||
return
|
||||
|
||||
print(f"\n{'=' * 70}")
|
||||
print(f"{title} ({len(issues)} total)")
|
||||
print("=" * 70)
|
||||
|
||||
for i, issue in enumerate(issues[:limit]):
|
||||
card_id = issue['card_id']
|
||||
card = cards.get(card_id, {})
|
||||
card_name = card.get('name', 'Unknown')
|
||||
|
||||
print(f"\n[{i+1}] {card_id} - {card_name}")
|
||||
print(f" Reason: {issue.get('reason', 'Unknown')}")
|
||||
|
||||
original = issue.get('original', {})
|
||||
if isinstance(original, dict):
|
||||
if original.get('trigger'):
|
||||
print(f" Trigger: {original['trigger'][:80]}...")
|
||||
if original.get('effect'):
|
||||
effect_text = original['effect']
|
||||
if len(effect_text) > 100:
|
||||
effect_text = effect_text[:100] + "..."
|
||||
print(f" Effect: {effect_text}")
|
||||
|
||||
if issue.get('parsed'):
|
||||
parsed = issue['parsed']
|
||||
print(f" Parsed type: {parsed.get('type')}")
|
||||
if parsed.get('effects'):
|
||||
print(f" Effects: {[e.get('type') for e in parsed['effects']]}")
|
||||
|
||||
if len(issues) > limit:
|
||||
print(f"\n... and {len(issues) - limit} more")
|
||||
|
||||
|
||||
def print_effect_analysis(abilities_data: dict, effect_type: str) -> None:
|
||||
"""Show all abilities with a specific effect type."""
|
||||
effect_type = effect_type.upper()
|
||||
abilities = abilities_data.get('abilities', {})
|
||||
|
||||
matches = []
|
||||
for card_id, card_abilities in abilities.items():
|
||||
for ability in card_abilities:
|
||||
parsed = ability.get('parsed', {})
|
||||
for effect in parsed.get('effects', []):
|
||||
if effect.get('type') == effect_type:
|
||||
matches.append({
|
||||
'card_id': card_id,
|
||||
'ability': ability,
|
||||
'effect': effect
|
||||
})
|
||||
|
||||
print(f"\n{'=' * 70}")
|
||||
print(f"EFFECT TYPE: {effect_type} ({len(matches)} occurrences)")
|
||||
print("=" * 70)
|
||||
|
||||
# Group by effect structure
|
||||
structures = defaultdict(list)
|
||||
for match in matches:
|
||||
effect = match['effect']
|
||||
# Create a structure key
|
||||
keys = sorted([k for k in effect.keys() if k != 'raw'])
|
||||
structure = tuple(keys)
|
||||
structures[structure].append(match)
|
||||
|
||||
print(f"\nStructure variations: {len(structures)}")
|
||||
for structure, examples in structures.items():
|
||||
print(f"\n Keys: {list(structure)}")
|
||||
print(f" Count: {len(examples)}")
|
||||
# Show one example
|
||||
ex = examples[0]
|
||||
print(f" Example ({ex['card_id']}): {ex['effect']}")
|
||||
|
||||
|
||||
def validate_card(abilities_data: dict, card_id: str, cards: dict) -> None:
|
||||
"""Show detailed validation for a specific card."""
|
||||
abilities = abilities_data.get('abilities', {})
|
||||
card_abilities = abilities.get(card_id, [])
|
||||
card = cards.get(card_id, {})
|
||||
|
||||
print(f"\n{'=' * 70}")
|
||||
print(f"CARD: {card_id} - {card.get('name', 'Unknown')}")
|
||||
print("=" * 70)
|
||||
|
||||
if not card_abilities:
|
||||
print("No abilities found for this card.")
|
||||
return
|
||||
|
||||
for i, ability in enumerate(card_abilities):
|
||||
print(f"\n--- Ability {i + 1} ---")
|
||||
|
||||
original = ability.get('original', {})
|
||||
print(f"Type: {original.get('type', 'unknown')}")
|
||||
if original.get('trigger'):
|
||||
print(f"Trigger: {original['trigger']}")
|
||||
if original.get('effect'):
|
||||
print(f"Effect: {original['effect']}")
|
||||
|
||||
print(f"\nConfidence: {ability.get('parse_confidence', 'UNKNOWN')}")
|
||||
|
||||
parsed = ability.get('parsed', {})
|
||||
print(f"Parsed Type: {parsed.get('type', 'none')}")
|
||||
|
||||
if parsed.get('trigger'):
|
||||
print(f"Parsed Trigger: {json.dumps(parsed['trigger'], indent=2)}")
|
||||
|
||||
if parsed.get('effects'):
|
||||
print("Parsed Effects:")
|
||||
for j, effect in enumerate(parsed['effects']):
|
||||
print(f" [{j+1}] {json.dumps(effect, indent=4)}")
|
||||
|
||||
|
||||
def export_issues(issues: dict, cards: dict, output_path: str) -> None:
|
||||
"""Export issues to a JSON file for external review/fixing."""
|
||||
export_data = {
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'summary': {
|
||||
'unparsed': len(issues['unparsed']),
|
||||
'low_confidence': len(issues['low_confidence']),
|
||||
'unknown_effects': len(issues['unknown_effects']),
|
||||
'missing_targets': len(issues['missing_targets']),
|
||||
'complex_conditions': len(issues['complex_conditions']),
|
||||
},
|
||||
'issues': {}
|
||||
}
|
||||
|
||||
# Group issues by card
|
||||
for issue_type, issue_list in issues.items():
|
||||
for issue in issue_list:
|
||||
card_id = issue['card_id']
|
||||
if card_id not in export_data['issues']:
|
||||
card = cards.get(card_id, {})
|
||||
export_data['issues'][card_id] = {
|
||||
'name': card.get('name', 'Unknown'),
|
||||
'problems': []
|
||||
}
|
||||
|
||||
export_data['issues'][card_id]['problems'].append({
|
||||
'type': issue_type,
|
||||
'ability_index': issue.get('ability_index', 0),
|
||||
'reason': issue.get('reason', ''),
|
||||
'original': issue.get('original', {})
|
||||
})
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(export_data, f, indent=2)
|
||||
|
||||
print(f"Exported {len(export_data['issues'])} cards with issues to: {output_path}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Validate processed abilities')
|
||||
parser.add_argument('--unparsed', action='store_true', help='Show unparsed abilities')
|
||||
parser.add_argument('--low-confidence', action='store_true', help='Show low confidence parses')
|
||||
parser.add_argument('--unknown', action='store_true', help='Show unknown effects')
|
||||
parser.add_argument('--card', type=str, help='Validate specific card by ID')
|
||||
parser.add_argument('--effect', type=str, help='Show all abilities with effect type')
|
||||
parser.add_argument('--summary', action='store_true', help='Show summary only')
|
||||
parser.add_argument('--export', type=str, help='Export issues to JSON file')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
abilities_data = load_abilities()
|
||||
if not abilities_data:
|
||||
return
|
||||
|
||||
cards = load_cards()
|
||||
issues = get_issues(abilities_data)
|
||||
|
||||
# Always show summary
|
||||
print_summary(abilities_data, issues)
|
||||
|
||||
if args.summary:
|
||||
return
|
||||
|
||||
if args.card:
|
||||
validate_card(abilities_data, args.card, cards)
|
||||
return
|
||||
|
||||
if args.effect:
|
||||
print_effect_analysis(abilities_data, args.effect)
|
||||
return
|
||||
|
||||
if args.export:
|
||||
export_issues(issues, cards, args.export)
|
||||
return
|
||||
|
||||
# Show specific issue types
|
||||
if args.unparsed:
|
||||
print_issues(issues['unparsed'], "UNPARSED ABILITIES", cards)
|
||||
|
||||
if args.low_confidence:
|
||||
print_issues(issues['low_confidence'], "LOW CONFIDENCE PARSES", cards)
|
||||
|
||||
if args.unknown:
|
||||
print_issues(issues['unknown_effects'], "UNKNOWN EFFECTS", cards)
|
||||
|
||||
# If no specific flag, show a sample of each issue type
|
||||
if not any([args.unparsed, args.low_confidence, args.unknown]):
|
||||
print("\nUse --unparsed, --low-confidence, --unknown, --card, or --effect for details")
|
||||
print("Use --export issues.json to export all issues for review")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
373
tools/ai_card_reviewer.py
Normal file
373
tools/ai_card_reviewer.py
Normal file
@@ -0,0 +1,373 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI Card Reviewer - Uses Claude's vision to validate and correct card data.
|
||||
|
||||
Usage:
|
||||
python tools/ai_card_reviewer.py # Review all unreviewed cards
|
||||
python tools/ai_card_reviewer.py --set 1 # Review only Opus 1 cards
|
||||
python tools/ai_card_reviewer.py --card 1-001H # Review a specific card
|
||||
python tools/ai_card_reviewer.py --limit 10 # Review only 10 cards
|
||||
python tools/ai_card_reviewer.py --dry-run # Don't save changes, just show what would change
|
||||
|
||||
Requires:
|
||||
pip install anthropic
|
||||
|
||||
Set your API key:
|
||||
export ANTHROPIC_API_KEY=your-key-here
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import anthropic
|
||||
except ImportError:
|
||||
print("Error: anthropic package not installed. Run: pip install anthropic")
|
||||
sys.exit(1)
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
CARDS_FILE = PROJECT_ROOT / "data" / "cards.json"
|
||||
REVIEWED_FILE = PROJECT_ROOT / "data" / "reviewed.json"
|
||||
SOURCE_CARDS_DIR = PROJECT_ROOT / "source-cards"
|
||||
|
||||
# Rate limiting
|
||||
REQUESTS_PER_MINUTE = 30
|
||||
REQUEST_DELAY = 60 / REQUESTS_PER_MINUTE
|
||||
|
||||
|
||||
def load_cards():
|
||||
with open(CARDS_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_cards(data):
|
||||
with open(CARDS_FILE, "w") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def load_reviewed():
|
||||
if REVIEWED_FILE.exists():
|
||||
with open(REVIEWED_FILE, "r") as f:
|
||||
return set(json.load(f).get("reviewed", []))
|
||||
return set()
|
||||
|
||||
|
||||
def save_reviewed(reviewed_set):
|
||||
with open(REVIEWED_FILE, "w") as f:
|
||||
json.dump({"reviewed": list(reviewed_set)}, f, indent=2)
|
||||
|
||||
|
||||
def encode_image(image_path: Path) -> tuple[str, str]:
|
||||
"""Encode image to base64 and return (data, media_type)."""
|
||||
with open(image_path, "rb") as f:
|
||||
data = base64.standard_b64encode(f.read()).decode("utf-8")
|
||||
|
||||
ext = image_path.suffix.lower()
|
||||
if ext in (".jpg", ".jpeg"):
|
||||
media_type = "image/jpeg"
|
||||
elif ext == ".png":
|
||||
media_type = "image/png"
|
||||
elif ext == ".gif":
|
||||
media_type = "image/gif"
|
||||
elif ext == ".webp":
|
||||
media_type = "image/webp"
|
||||
else:
|
||||
media_type = "image/jpeg" # fallback
|
||||
|
||||
return data, media_type
|
||||
|
||||
|
||||
SYSTEM_PROMPT = """You are a data validator for Final Fantasy Trading Card Game (FFTCG) cards.
|
||||
You will be shown a card image and its current JSON data. Your job is to:
|
||||
|
||||
1. Verify all fields match what's visible on the card
|
||||
2. Correct any errors you find
|
||||
3. Fill in any missing data
|
||||
4. Ensure abilities are accurately transcribed
|
||||
|
||||
FFTCG Card Structure:
|
||||
- id: Card number (e.g., "1-001H" = Opus 1, card 001, Hero rarity)
|
||||
- name: Character/card name
|
||||
- type: Forward, Backup, Summon, or Monster
|
||||
- element: Fire, Ice, Wind, Earth, Lightning, Water, Light, or Dark (can be array for multi-element)
|
||||
- cost: Crystal Point cost (number in top-left)
|
||||
- power: Power value for Forwards (number at bottom, in thousands like 7000). Backups/Summons have null power.
|
||||
- job: Job class (e.g., "Warrior", "Knight")
|
||||
- category: Game title (e.g., "VII", "X", "TACTICS")
|
||||
- is_generic: true if card has no category (generic cards)
|
||||
- has_ex_burst: true if card has EX BURST ability (lightning bolt icon)
|
||||
- has_haste: true if the card has the Haste keyword ability (can attack/use abilities the turn it enters)
|
||||
|
||||
Ability Types:
|
||||
- field: Passive abilities always active (includes keyword abilities like Haste, Brave, First Strike)
|
||||
- auto: Triggered abilities (start with "When..." or have a trigger condition)
|
||||
- action: Activated abilities (have a cost, often require dulling with S symbol)
|
||||
- special: Special abilities (usually named abilities with S symbol cost)
|
||||
|
||||
Important Keywords to Identify:
|
||||
- Haste: "This card can attack and use abilities the turn it enters the field" - set has_haste=true
|
||||
- Brave: Card doesn't dull when attacking
|
||||
- First Strike: Deals damage before opponent in combat
|
||||
|
||||
Respond ONLY with valid JSON in this exact format:
|
||||
{
|
||||
"changes_made": true/false,
|
||||
"confidence": "high"/"medium"/"low",
|
||||
"notes": "Brief explanation of changes or issues",
|
||||
"corrected_data": {
|
||||
// Complete card JSON with all fields
|
||||
}
|
||||
}
|
||||
|
||||
If the data looks correct, set changes_made to false and return the original data in corrected_data.
|
||||
Always include ALL fields in corrected_data, even if unchanged."""
|
||||
|
||||
|
||||
def review_card(client: anthropic.Anthropic, card: dict, image_path: Path) -> dict:
|
||||
"""Review a single card using Claude's vision."""
|
||||
|
||||
image_data, media_type = encode_image(image_path)
|
||||
|
||||
user_message = f"""Please review this FFTCG card image and verify/correct the following JSON data:
|
||||
|
||||
```json
|
||||
{json.dumps(card, indent=2)}
|
||||
```
|
||||
|
||||
Look carefully at:
|
||||
1. Card name spelling
|
||||
2. Element (color of the crystal/card border)
|
||||
3. Cost (number in the crystal)
|
||||
4. Power (number at bottom for Forwards, should be null for Backups/Summons)
|
||||
5. Job and Category text
|
||||
6. All abilities - check type, name, trigger, effect text
|
||||
7. EX BURST indicator (lightning bolt symbol)
|
||||
8. Haste keyword - if the card mentions attacking or using abilities the turn it enters, has_haste should be true
|
||||
|
||||
Return the corrected JSON."""
|
||||
|
||||
try:
|
||||
response = client.messages.create(
|
||||
model="claude-opus-4-5-20251101",
|
||||
max_tokens=4096,
|
||||
system=SYSTEM_PROMPT,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": media_type,
|
||||
"data": image_data,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": user_message,
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
# Extract JSON from response
|
||||
response_text = response.content[0].text
|
||||
|
||||
# Try to parse JSON (handle markdown code blocks)
|
||||
if "```json" in response_text:
|
||||
json_str = response_text.split("```json")[1].split("```")[0].strip()
|
||||
elif "```" in response_text:
|
||||
json_str = response_text.split("```")[1].split("```")[0].strip()
|
||||
else:
|
||||
json_str = response_text.strip()
|
||||
|
||||
return json.loads(json_str)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f" JSON parse error: {e}")
|
||||
print(f" Response: {response_text[:500]}...")
|
||||
return None
|
||||
except anthropic.APIError as e:
|
||||
print(f" API error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def print_diff(original: dict, corrected: dict, card_id: str):
|
||||
"""Print differences between original and corrected card data."""
|
||||
changes = []
|
||||
|
||||
# Compare top-level fields
|
||||
for key in set(list(original.keys()) + list(corrected.keys())):
|
||||
if key in ("abilities", "image"):
|
||||
continue
|
||||
orig_val = original.get(key)
|
||||
corr_val = corrected.get(key)
|
||||
if orig_val != corr_val:
|
||||
changes.append(f" {key}: {orig_val!r} -> {corr_val!r}")
|
||||
|
||||
# Compare abilities (simplified)
|
||||
orig_abilities = original.get("abilities", [])
|
||||
corr_abilities = corrected.get("abilities", [])
|
||||
|
||||
if len(orig_abilities) != len(corr_abilities):
|
||||
changes.append(f" abilities: {len(orig_abilities)} -> {len(corr_abilities)} abilities")
|
||||
else:
|
||||
for i, (orig_ab, corr_ab) in enumerate(zip(orig_abilities, corr_abilities)):
|
||||
if orig_ab != corr_ab:
|
||||
changes.append(f" abilities[{i}]: modified")
|
||||
|
||||
if changes:
|
||||
print(f"\n[{card_id}] Changes:")
|
||||
for change in changes:
|
||||
print(change)
|
||||
else:
|
||||
print(f"[{card_id}] No changes needed")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="AI Card Reviewer using Claude Vision")
|
||||
parser.add_argument("--set", type=str, help="Only review cards from this set/opus (e.g., '1' for Opus 1)")
|
||||
parser.add_argument("--card", type=str, help="Review a specific card by ID")
|
||||
parser.add_argument("--limit", type=int, help="Maximum number of cards to review")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Don't save changes, just show what would change")
|
||||
parser.add_argument("--include-reviewed", action="store_true", help="Re-review already reviewed cards")
|
||||
parser.add_argument("--auto-mark-reviewed", action="store_true", help="Automatically mark cards as reviewed after AI review")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check API key
|
||||
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
||||
if not api_key:
|
||||
print("Error: ANTHROPIC_API_KEY environment variable not set")
|
||||
print("Set it with: export ANTHROPIC_API_KEY=your-key-here")
|
||||
sys.exit(1)
|
||||
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
|
||||
# Load data
|
||||
cards_data = load_cards()
|
||||
reviewed_set = load_reviewed()
|
||||
all_cards = cards_data["cards"]
|
||||
|
||||
print(f"Loaded {len(all_cards)} cards, {len(reviewed_set)} already reviewed")
|
||||
|
||||
# Filter cards to review
|
||||
cards_to_review = []
|
||||
for card in all_cards:
|
||||
# Skip if already reviewed (unless --include-reviewed)
|
||||
if not args.include_reviewed and card["id"] in reviewed_set:
|
||||
continue
|
||||
|
||||
# Filter by set if specified
|
||||
if args.set and not card["id"].startswith(args.set + "-"):
|
||||
continue
|
||||
|
||||
# Filter by specific card if specified
|
||||
if args.card and card["id"] != args.card:
|
||||
continue
|
||||
|
||||
# Check image exists
|
||||
image_path = SOURCE_CARDS_DIR / card.get("image", "")
|
||||
if not image_path.exists():
|
||||
print(f"Warning: Image not found for {card['id']}: {image_path}")
|
||||
continue
|
||||
|
||||
cards_to_review.append((card, image_path))
|
||||
|
||||
# Apply limit
|
||||
if args.limit:
|
||||
cards_to_review = cards_to_review[:args.limit]
|
||||
|
||||
if not cards_to_review:
|
||||
print("No cards to review matching criteria")
|
||||
return
|
||||
|
||||
print(f"\nReviewing {len(cards_to_review)} cards...")
|
||||
if args.dry_run:
|
||||
print("(DRY RUN - no changes will be saved)")
|
||||
print()
|
||||
|
||||
# Track statistics
|
||||
stats = {
|
||||
"reviewed": 0,
|
||||
"changed": 0,
|
||||
"errors": 0,
|
||||
"high_confidence": 0,
|
||||
"medium_confidence": 0,
|
||||
"low_confidence": 0,
|
||||
}
|
||||
|
||||
# Review each card
|
||||
for i, (card, image_path) in enumerate(cards_to_review):
|
||||
print(f"[{i+1}/{len(cards_to_review)}] Reviewing {card['id']}: {card.get('name', 'Unknown')}...", end="", flush=True)
|
||||
|
||||
result = review_card(client, card, image_path)
|
||||
|
||||
if result is None:
|
||||
print(" ERROR")
|
||||
stats["errors"] += 1
|
||||
time.sleep(REQUEST_DELAY)
|
||||
continue
|
||||
|
||||
stats["reviewed"] += 1
|
||||
confidence = result.get("confidence", "unknown")
|
||||
stats[f"{confidence}_confidence"] = stats.get(f"{confidence}_confidence", 0) + 1
|
||||
|
||||
if result.get("changes_made"):
|
||||
stats["changed"] += 1
|
||||
print(f" CHANGED ({confidence} confidence)")
|
||||
print(f" Notes: {result.get('notes', 'No notes')}")
|
||||
|
||||
corrected = result.get("corrected_data", {})
|
||||
print_diff(card, corrected, card["id"])
|
||||
|
||||
if not args.dry_run:
|
||||
# Update card in data
|
||||
for j, c in enumerate(cards_data["cards"]):
|
||||
if c["id"] == card["id"]:
|
||||
# Preserve image path
|
||||
corrected["image"] = card.get("image", "")
|
||||
cards_data["cards"][j] = corrected
|
||||
break
|
||||
else:
|
||||
print(f" OK ({confidence} confidence)")
|
||||
|
||||
# Mark as reviewed if auto-mark enabled
|
||||
if args.auto_mark_reviewed and not args.dry_run:
|
||||
reviewed_set.add(card["id"])
|
||||
|
||||
# Rate limiting
|
||||
if i < len(cards_to_review) - 1:
|
||||
time.sleep(REQUEST_DELAY)
|
||||
|
||||
# Save changes
|
||||
if not args.dry_run and stats["changed"] > 0:
|
||||
print(f"\nSaving changes to {CARDS_FILE}...")
|
||||
save_cards(cards_data)
|
||||
|
||||
if not args.dry_run and args.auto_mark_reviewed:
|
||||
print(f"Saving reviewed status to {REVIEWED_FILE}...")
|
||||
save_reviewed(reviewed_set)
|
||||
|
||||
# Print summary
|
||||
print(f"\n{'='*50}")
|
||||
print("Summary:")
|
||||
print(f" Cards reviewed: {stats['reviewed']}")
|
||||
print(f" Cards changed: {stats['changed']}")
|
||||
print(f" Errors: {stats['errors']}")
|
||||
print(f" High confidence: {stats['high_confidence']}")
|
||||
print(f" Medium confidence: {stats['medium_confidence']}")
|
||||
print(f" Low confidence: {stats['low_confidence']}")
|
||||
|
||||
if args.dry_run and stats["changed"] > 0:
|
||||
print(f"\n(Dry run - {stats['changed']} changes NOT saved)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -337,6 +337,10 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
||||
<input type="checkbox" id="fieldExBurst" data-field="has_ex_burst">
|
||||
<label for="fieldExBurst">EX Burst</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="fieldHaste" data-field="has_haste">
|
||||
<label for="fieldHaste">Haste</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="abilities-section">
|
||||
@@ -585,6 +589,7 @@ function showCardInModal(index) {
|
||||
document.getElementById('fieldCategory').value = card.category || '';
|
||||
document.getElementById('fieldGeneric').checked = !!card.is_generic;
|
||||
document.getElementById('fieldExBurst').checked = !!card.has_ex_burst;
|
||||
document.getElementById('fieldHaste').checked = !!card.has_haste;
|
||||
|
||||
const abList = document.getElementById('abilitiesList');
|
||||
abList.innerHTML = '';
|
||||
@@ -683,6 +688,7 @@ function getCurrentEdits() {
|
||||
category: document.getElementById('fieldCategory').value,
|
||||
is_generic: document.getElementById('fieldGeneric').checked,
|
||||
has_ex_burst: document.getElementById('fieldExBurst').checked,
|
||||
has_haste: document.getElementById('fieldHaste').checked,
|
||||
abilities: getAbilitiesFromDOM(),
|
||||
image: card.image,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user