"""Erzeugt plausible Beispiel-Sichtungen, damit sich die Oberfläche vor dem ersten echten Vogel beurteilen lässt. Bewusst kein Teil des Normalbetriebs: ``--clear`` räumt alles wieder weg. Die Bilder sind die Referenzfotos aus Wikipedia, keine echten Kameraaufnahmen — die Sichtungen sind also erkennbar Attrappe. """ from __future__ import annotations import random from datetime import timedelta from django.core.files.base import ContentFile from django.core.management.base import BaseCommand from django.utils import timezone from dex import scoring from dex.models import DexEntry, EarnedBadge, Sighting, Species # Wie wahrscheinlich taucht eine Seltenheitsstufe auf? Grob an einem echten # Garten orientiert: Kohlmeisen im Dauerbetrieb, der Sperber einmal im Monat. RARITY_WEIGHT = { "haeufig": 60, "gelegentlich": 22, "selten": 6, "episch": 1.2, "legendaer": 0.3, } # Vögel am Futterhaus haben zwei Stoßzeiten: kurz nach Sonnenaufgang und am # späten Nachmittag. HOUR_WEIGHT = [ 0.1, 0.1, 0.1, 0.2, 0.6, 2.5, 6.0, 9.0, 8.0, 6.0, 4.5, 3.5, 3.0, 3.2, 4.0, 5.5, 7.0, 6.5, 4.0, 1.5, 0.5, 0.2, 0.1, 0.1, ] class Command(BaseCommand): help = "Legt Beispiel-Sichtungen an (nur zum Ansehen der Oberfläche)." def add_arguments(self, parser): parser.add_argument("--tage", type=int, default=21) parser.add_argument("--pro-tag", type=int, default=9) parser.add_argument( "--clear", action="store_true", help="Alle Sichtungen, Dex-Einträge und Abzeichen löschen.", ) parser.add_argument("--seed", type=int, default=1312) def handle(self, *args, **opts): if opts["clear"]: n = Sighting.objects.count() Sighting.objects.all().delete() DexEntry.objects.all().delete() EarnedBadge.objects.all().delete() self.stdout.write(self.style.SUCCESS(f"{n} Sichtungen gelöscht.")) return if Sighting.objects.exists(): self.stdout.write( self.style.WARNING( "Es gibt bereits Sichtungen. Erst 'demo_data --clear' ausführen, " "sonst mischen sich Demo- und Echtdaten." ) ) return species = list(Species.objects.all()) if not species: self.stdout.write(self.style.ERROR("Katalog leer — erst 'seed_dex'.")) return rng = random.Random(opts["seed"]) weights = [RARITY_WEIGHT.get(s.rarity, 1) for s in species] # Chronologisch anlegen, damit Erstfänge, XP und Abzeichen in der # richtigen Reihenfolge verbucht werden. now = timezone.localtime() events = [] for day_offset in range(opts["tage"], -1, -1): day = (now - timedelta(days=day_offset)).date() for _ in range(rng.randint(max(1, opts["pro_tag"] - 4), opts["pro_tag"] + 4)): hour = rng.choices(range(24), weights=HOUR_WEIGHT)[0] start = timezone.make_aware( timezone.datetime.combine(day, timezone.datetime.min.time()) ) + timedelta(hours=hour, minutes=rng.randint(0, 59), seconds=rng.randint(0, 59)) if start > timezone.now(): continue events.append(start) events.sort() created = 0 for start in events: sp = rng.choices(species, weights=weights)[0] # Ein Teil der Besuche bleibt unbestimmt — so sieht es im Betrieb # auch aus, und die Oberfläche muss den Fall aushalten. unbestimmt = rng.random() < 0.12 confidence = 0.0 if unbestimmt else rng.uniform(0.55, 0.99) duration = rng.uniform(3, 75) sighting = Sighting( species=None if unbestimmt else sp, confidence=confidence, started_at=start, ended_at=start + timedelta(seconds=duration), frames_total=int(duration * 4), frames_with_bird=int(duration * 4 * rng.uniform(0.5, 0.95)), votes={sp.slug: round(confidence, 3)} if not unbestimmt else {}, mean_bird_score=rng.uniform(0.4, 0.95), ) if not unbestimmt and sp.reference_image: try: sp.reference_image.open("rb") data = sp.reference_image.read() sp.reference_image.close() sighting.image.save(f"demo-{sp.slug}.jpg", ContentFile(data), save=False) sighting.crop.save(f"demo-{sp.slug}.jpg", ContentFile(data), save=False) except OSError: pass sighting.save() scoring.register(sighting) created += 1 p = scoring.progress() self.stdout.write( self.style.SUCCESS( f"{created} Beispiel-Sichtungen angelegt. " f"Level {p.level} ({p.rank}), {p.xp} XP, " f"{p.species_seen}/{p.species_total} Arten, {p.badges} Abzeichen." ) )