first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:26:46 +03:00
commit 2be3a313ad
55 changed files with 3609 additions and 0 deletions

0
namecreate/__init__.py Normal file
View File

250
namecreate/admin.py Normal file
View File

@@ -0,0 +1,250 @@
import io
from django.contrib import admin
from django.contrib import messages
from django.core.management import call_command
from django.http import HttpResponseRedirect
from django.urls import path, reverse
from django.utils.html import format_html
from .models import TrainingJob, NameVocab, GeneratedPerson
class GeneratedPersonInline(admin.TabularInline):
model = GeneratedPerson
extra = 0
fields = (
'first_name', 'last_name', 'username', 'username_locked',
'birth_date', 'gender', 'confidence', 'generated_at'
)
readonly_fields = (
'first_name', 'last_name', 'username', 'username_locked',
'birth_date', 'gender', 'confidence', 'generated_at'
)
can_delete = False
show_change_link = True
@admin.register(TrainingJob)
class TrainingJobAdmin(admin.ModelAdmin):
list_display = (
'id',
'task_id',
'status',
'model_type',
'model_file_exists',
'generated_person_count',
'accuracy',
'created_at',
'completed_at',
)
list_filter = ('status', 'model_type', 'created_at')
search_fields = ('task_id', 'error_message')
readonly_fields = (
'task_id',
'created_at',
'started_at',
'completed_at',
'model_version',
'model_file_exists',
'generated_person_count',
)
actions = (
'generate_100_statistical_action',
'generate_100_llm_action',
'generate_1000_statistical_action',
'generate_1000_llm_action',
)
inlines = (GeneratedPersonInline,)
fieldsets = (
('Görev Bilgisi', {
'fields': ('task_id', 'status', 'created_at', 'started_at', 'completed_at')
}),
('Model', {
'fields': ('model_type', 'model_version', 'model_path', 'model_file_exists')
}),
('Uretilen Kayitlar', {
'fields': ('generated_person_count',)
}),
('Metrikler', {
'fields': ('accuracy', 'precision', 'recall', 'f1_score')
}),
('Go Servisi', {
'fields': ('go_service_notified',)
}),
('Hata', {
'fields': ('error_message',),
'classes': ('collapse',)
}),
)
def model_file_exists(self, obj):
return bool(obj.model_path and __import__('os').path.exists(obj.model_path))
model_file_exists.boolean = True
model_file_exists.short_description = 'Model dosyasi var'
def generated_person_count(self, obj):
return obj.generated_persons.count()
generated_person_count.short_description = 'Uretilen kisi sayisi'
def _run_generate_persons(self, request, queryset, count, use_llm):
total_jobs = queryset.count()
ok_jobs = 0
for job in queryset:
try:
out = io.StringIO()
kwargs = {
'count': count,
'job_id': job.pk,
'stdout': out,
}
if use_llm:
kwargs['use_llm'] = True
else:
kwargs['no_llm'] = True
call_command('generate_persons', **kwargs)
ok_jobs += 1
except Exception as exc:
self.message_user(
request,
f'Job id={job.pk} icin uretim hatasi: {exc}',
level=messages.ERROR,
)
mode = 'LLM (fallback ile)' if use_llm else 'Istatistiksel'
self.message_user(
request,
f'Uretim tamamlandi. Mod: {mode}. Basarili job: {ok_jobs}/{total_jobs}. Her job icin {count} kayit.',
level=messages.SUCCESS,
)
@admin.action(description='Secili job(lar) icin 100 kisi uret (Istatistiksel)')
def generate_100_statistical_action(self, request, queryset):
self._run_generate_persons(request, queryset, count=100, use_llm=False)
@admin.action(description='Secili job(lar) icin 100 kisi uret (LLM + fallback)')
def generate_100_llm_action(self, request, queryset):
self._run_generate_persons(request, queryset, count=100, use_llm=True)
@admin.action(description='Secili job(lar) icin 1000 kisi uret (Istatistiksel)')
def generate_1000_statistical_action(self, request, queryset):
self._run_generate_persons(request, queryset, count=1000, use_llm=False)
@admin.action(description='Secili job(lar) icin 1000 kisi uret (LLM + fallback)')
def generate_1000_llm_action(self, request, queryset):
self._run_generate_persons(request, queryset, count=1000, use_llm=True)
@admin.register(NameVocab)
class NameVocabAdmin(admin.ModelAdmin):
list_display = ('name', 'name_type', 'gender', 'origin', 'frequency')
list_filter = ('origin', 'name_type', 'gender')
search_fields = ('name',)
ordering = ('origin', '-frequency')
@admin.register(GeneratedPerson)
class GeneratedPersonAdmin(admin.ModelAdmin):
list_display = (
'first_name', 'last_name', 'username', 'username_locked',
'birth_date', 'gender', 'confidence', 'generated_at', 'username_ops'
)
list_filter = ('gender', 'username_locked', 'generated_at')
search_fields = ('first_name', 'last_name', 'username')
readonly_fields = ('generated_at',)
actions = ('regenerate_username_action', 'lock_username_action', 'unlock_username_action')
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
'<int:person_id>/regenerate-username/',
self.admin_site.admin_view(self.regenerate_username_view),
name='namecreate_generatedperson_regenerate_username',
),
path(
'<int:person_id>/toggle-username-lock/',
self.admin_site.admin_view(self.toggle_username_lock_view),
name='namecreate_generatedperson_toggle_username_lock',
),
]
return custom_urls + urls
def regenerate_username_view(self, request, person_id):
person = self.get_object(request, person_id)
if person is None:
self.message_user(request, 'Kayit bulunamadi.', level=messages.ERROR)
return HttpResponseRedirect('../')
if person.username_locked:
self.message_user(request, 'Username kilitli. Once kilidi acin.', level=messages.WARNING)
return HttpResponseRedirect('../../')
old_username = person.username
person.regenerate_username(save=True)
self.message_user(
request,
f'Username guncellendi: {old_username} -> {person.username}',
level=messages.SUCCESS,
)
return HttpResponseRedirect('../../')
def toggle_username_lock_view(self, request, person_id):
person = self.get_object(request, person_id)
if person is None:
self.message_user(request, 'Kayit bulunamadi.', level=messages.ERROR)
return HttpResponseRedirect('../')
person.username_locked = not person.username_locked
person.save(update_fields=['username_locked'])
state = 'kilitlendi' if person.username_locked else 'kilidi acildi'
self.message_user(request, f'Username {state}.', level=messages.SUCCESS)
return HttpResponseRedirect('../../')
def username_ops(self, obj):
regen_url = reverse('admin:namecreate_generatedperson_regenerate_username', args=[obj.pk])
lock_url = reverse('admin:namecreate_generatedperson_toggle_username_lock', args=[obj.pk])
lock_label = 'Kilidi ac' if obj.username_locked else 'Kilitle'
return format_html(
'<a class="button" href="{}">Yeniden uret</a>&nbsp;'
'<a class="button" href="{}">{}</a>',
regen_url,
lock_url,
lock_label,
)
username_ops.short_description = 'Islemler'
@admin.action(description='Secili kayitlarda username yeniden uret')
def regenerate_username_action(self, request, queryset):
used_usernames = set(
GeneratedPerson.objects.exclude(username='').values_list('username', flat=True)
)
updated = 0
skipped = 0
for person in queryset:
if person.username_locked:
skipped += 1
continue
used_usernames.discard(person.username)
person.regenerate_username(used_usernames=used_usernames, force=True, save=True)
updated += 1
self.message_user(
request,
f'{updated} kayitta username yeniden uretildi. Kilitli oldugu icin atlanan: {skipped}.'
)
@admin.action(description='Secili kayitlarda username kilitle')
def lock_username_action(self, request, queryset):
count = queryset.update(username_locked=True)
self.message_user(request, f'{count} kayitta username kilitlendi.')
@admin.action(description='Secili kayitlarda username kilidini ac')
def unlock_username_action(self, request, queryset):
count = queryset.update(username_locked=False)
self.message_user(request, f'{count} kayitta username kilidi acildi.')

5
namecreate/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class NamecreateConfig(AppConfig):
name = 'namecreate'

110
namecreate/llm_generator.py Normal file
View File

@@ -0,0 +1,110 @@
import json
from datetime import date, datetime
import requests
from django.conf import settings
def _build_prompt(count, min_age):
max_year = date.today().year - min_age
return (
"Turkiye icin gercekci kisi verisi uret. "
"Turkce isim ve soyisim oncelikli olsun. "
f"{count} adet kayit uret. "
f"Her kaydin dogum yili en fazla {max_year} olsun (yani en az {min_age} yas). "
"Sadece JSON dondur. Baska metin yazma. "
"Format tam olarak su olsun: "
"{\"people\":[{\"first_name\":\"...\",\"last_name\":\"...\",\"gender\":\"E|K\",\"birth_date\":\"YYYY-MM-DD\"}]}"
)
def _validate_people(items, min_age):
valid = []
today = date.today()
for item in items:
first_name = str(item.get('first_name', '')).strip()
last_name = str(item.get('last_name', '')).strip()
gender = str(item.get('gender', '')).strip().upper()
birth_date_raw = str(item.get('birth_date', '')).strip()
if not first_name or not last_name:
continue
if gender not in {'E', 'K'}:
continue
try:
birth_date = datetime.strptime(birth_date_raw, '%Y-%m-%d').date()
except ValueError:
continue
age = today.year - birth_date.year - ((today.month, today.day) < (birth_date.month, birth_date.day))
if age < min_age:
continue
valid.append({
'first_name': first_name,
'last_name': last_name,
'gender': gender,
'birth_date': birth_date,
})
return valid
def generate_people_with_llm(count, min_age=20):
"""LLM API'den kisi verisi alir, validate edip dondurur."""
api_url = getattr(settings, 'LLM_API_URL', None)
model = getattr(settings, 'LLM_MODEL', None)
timeout = getattr(settings, 'LLM_TIMEOUT', 30)
api_key = getattr(settings, 'LLM_API_KEY', None)
if not api_url or not model:
raise RuntimeError('LLM_API_URL veya LLM_MODEL ayari eksik.')
prompt = _build_prompt(count=count, min_age=min_age)
headers = {'Content-Type': 'application/json'}
if api_key:
headers['Authorization'] = f'Bearer {api_key}'
# OpenAI uyumlu endpoint
if '/v1/chat/completions' in api_url:
payload = {
'model': model,
'response_format': {'type': 'json_object'},
'messages': [
{'role': 'system', 'content': 'JSON disinda metin uretme.'},
{'role': 'user', 'content': prompt},
],
'temperature': 0.8,
}
resp = requests.post(api_url, headers=headers, json=payload, timeout=timeout)
resp.raise_for_status()
content = resp.json()['choices'][0]['message']['content']
else:
# Varsayilan: Ollama /api/chat
payload = {
'model': model,
'stream': False,
'format': 'json',
'messages': [
{'role': 'system', 'content': 'JSON disinda metin uretme.'},
{'role': 'user', 'content': prompt},
],
}
resp = requests.post(api_url, headers=headers, json=payload, timeout=timeout)
resp.raise_for_status()
body = resp.json()
content = body.get('message', {}).get('content') or body.get('response')
if not content:
raise RuntimeError('LLM bos cevap dondurdu.')
parsed = json.loads(content)
people = parsed.get('people') if isinstance(parsed, dict) else None
if not isinstance(people, list):
raise RuntimeError('LLM cevabi beklenen JSON formatinda degil.')
valid_people = _validate_people(people, min_age=min_age)
if not valid_people:
raise RuntimeError('LLM gecerli kisi verisi dondurmedi.')
return valid_people

View File

View File

@@ -0,0 +1,204 @@
"""
NameVocab tablosundan ağırlıklı örneklemeyle kişi verisi üretir.
- Doğum tarihi: 20 yaş ve üstü (bugüne göre)
- Türkçe isimler frekans ağırlıklı olarak önce gelir
- Üretilen kayıtlar GeneratedPerson tablosuna kaydedilir
Kullanım:
python manage.py generate_persons # 1000 kişi
python manage.py generate_persons --count 500 # 500 kişi
python manage.py generate_persons --clear # önce tabloyu temizle
"""
import random
import calendar
from datetime import date
from django.core.management.base import BaseCommand, CommandError
from namecreate.models import NameVocab, GeneratedPerson, TrainingJob
from namecreate.username_utils import build_unique_username
from namecreate.llm_generator import generate_people_with_llm
# Bugün: 27 Mart 2026 — 20 yaş ve üstü → en geç 27 Mart 2006 doğum
MAX_BIRTH_YEAR = date.today().year - 20 # 2006
MIN_BIRTH_YEAR = 1940
def _weighted_pick(queryset):
"""Frequency değerlerine göre ağırlıklı rastgele seçim."""
names = list(queryset.values_list('name', 'frequency'))
if not names:
raise CommandError("NameVocab tablosu boş. Önce 'seed_name_vocab' komutunu çalıştırın.")
population = [n for n, _ in names]
weights = [f for _, f in names]
return random.choices(population, weights=weights, k=1)[0]
def _random_birth_date():
"""20 yaş ve üstü rastgele doğum tarihi üretir."""
year = random.randint(MIN_BIRTH_YEAR, MAX_BIRTH_YEAR)
month = random.randint(1, 12)
# Ayın son gününü doğru hesapla (şubat, 30/31 gün farkları)
_, max_day = calendar.monthrange(year, month)
# 2006 doğumsa mart ayı sonrasında doğanlar henüz 20 yaşında değil
if year == MAX_BIRTH_YEAR:
today = date.today()
if month > today.month:
month = random.randint(1, today.month)
if month == today.month:
_, max_day = calendar.monthrange(year, month)
max_day = min(max_day, today.day)
_, max_day = calendar.monthrange(year, month)
day = random.randint(1, max_day)
return date(year, month, day)
class Command(BaseCommand):
help = '1000 kişilik sahte isim/soyisim/doğum tarihi verisi üretir (20 yaş ve üstü)'
def add_arguments(self, parser):
parser.add_argument(
'--count', type=int, default=1000,
help='Üretilecek kişi sayısı (varsayılan: 1000)'
)
parser.add_argument(
'--clear', action='store_true',
help='Üretmeden önce mevcut GeneratedPerson kayıtlarını sil'
)
parser.add_argument(
'--job-id', type=int, default=None,
help='Bağlanacak TrainingJob ID (belirtilmezse en son tamamlanmış iş kullanılır)'
)
parser.add_argument(
'--use-llm', action='store_true',
help='Uretimi LLM ile yapmayi dener; basarisiz olursa istatistiksel fallback yapar.'
)
parser.add_argument(
'--no-llm', action='store_true',
help='LLM denemeyi kapatir, dogrudan istatistiksel uretim yapar.'
)
parser.add_argument(
'--llm-strict', action='store_true',
help='LLM basarisiz olursa fallback yapma, komutu hata ile sonlandir.'
)
def handle(self, *args, **options):
count = options['count']
if options['clear']:
deleted, _ = GeneratedPerson.objects.all().delete()
self.stdout.write(self.style.WARNING(f'{deleted} eski kayıt silindi.'))
# TrainingJob bağlantısını belirle
job_id = options.get('job_id')
if job_id:
try:
training_job = TrainingJob.objects.get(pk=job_id)
except TrainingJob.DoesNotExist:
raise CommandError(f'TrainingJob id={job_id} bulunamadı.')
else:
training_job = TrainingJob.objects.filter(status='completed').exclude(
model_path=None
).order_by('-completed_at').first()
if training_job is None:
self.stdout.write(self.style.WARNING(
'Tamamlanmış (model dosyalı) TrainingJob bulunamadı. training_job=None olarak üretilecek.'
))
else:
self.stdout.write(f'TrainingJob kullanılıyor: id={training_job.pk} ({training_job.model_type})')
# Erkek / kadın isimlerini bir kez çek
male_first = NameVocab.objects.filter(name_type='first', gender='E')
female_first = NameVocab.objects.filter(name_type='first', gender='K')
last_names = NameVocab.objects.filter(name_type='last')
if not male_first.exists() or not female_first.exists() or not last_names.exists():
raise CommandError(
"NameVocab tablosu eksik. Önce 'python manage.py seed_name_vocab' komutunu çalıştırın."
)
# Tüm ağırlıklı listeleri RAM'e al (1000 kayıt için yeterli)
male_pool = list(male_first.values_list('name', 'frequency'))
female_pool = list(female_first.values_list('name', 'frequency'))
last_pool = list(last_names.values_list('name', 'frequency'))
def pick(pool):
names, weights = zip(*pool)
return random.choices(names, weights=weights, k=1)[0]
used_usernames = set(GeneratedPerson.objects.values_list('username', flat=True).exclude(username=''))
persons = []
source = 'statistical'
llm_rows = None
use_llm = options.get('use_llm') or not options.get('no_llm')
if use_llm:
try:
llm_rows = generate_people_with_llm(count=count, min_age=20)
source = 'llm'
except Exception as e:
if options.get('llm_strict'):
raise CommandError(f'LLM uretimi basarisiz: {e}')
self.stdout.write(self.style.WARNING(
f'LLM uretimi basarisiz ({e}). Istatistiksel fallback kullaniliyor.'
))
if llm_rows:
for row in llm_rows[:count]:
first = row['first_name']
last = row['last_name']
gender = row['gender']
birth = row['birth_date']
username = build_unique_username(first, last, birth, used_usernames)
persons.append(GeneratedPerson(
first_name=first,
last_name=last.upper(),
username=username,
birth_date=birth,
gender=gender,
confidence=None,
training_job=training_job,
))
missing = count - len(persons)
for _ in range(missing):
gender = random.choice(['E', 'K'])
first = pick(male_pool if gender == 'E' else female_pool)
last = pick(last_pool)
birth = _random_birth_date()
username = build_unique_username(first, last, birth, used_usernames)
persons.append(GeneratedPerson(
first_name=first,
last_name=last.upper(), # soyisim büyük harf
username=username,
birth_date=birth,
gender=gender,
confidence=None,
training_job=training_job,
))
GeneratedPerson.objects.bulk_create(persons)
self.stdout.write(self.style.SUCCESS(
f'\n{count} kişi başarıyla üretildi ve kaydedildi.'
))
self.stdout.write(f' Kaynak: {source}')
self.stdout.write(
f" Erkek: {sum(1 for p in persons if p.gender == 'E')} | "
f"Kadın: {sum(1 for p in persons if p.gender == 'K')}"
)
self.stdout.write(
f" Doğum aralığı: {MIN_BIRTH_YEAR} {MAX_BIRTH_YEAR} (20 yaş ve üstü)"
)
# Örnek 5 kayıt göster
self.stdout.write('\nİlk 5 örnek:')
for p in persons[:5]:
age = date.today().year - p.birth_date.year - (
(date.today().month, date.today().day) < (p.birth_date.month, p.birth_date.day)
)
self.stdout.write(
f" {p.first_name} {p.last_name} (@{p.username}) | {p.birth_date} | "
f"{p.get_gender_display()} | {age} yaş"
)

View File

@@ -0,0 +1,125 @@
"""
Türkçe kökenli isimler öncelikli olarak NameVocab tablosunu doldurur.
Kullanım: python manage.py seed_name_vocab
"""
from django.core.management.base import BaseCommand
from namecreate.models import NameVocab
# -----------------------------------------------------------------------
# Türkçe kökenli isimler — en yüksek öncelik
# -----------------------------------------------------------------------
TURKCE_ERKEK = [
("Alp", 80), ("Alpay", 60), ("Alperen", 100), ("Altan", 70),
("Aydın", 90), ("Baran", 85), ("Barış", 120), ("Batuhan", 95),
("Berk", 75), ("Berkay", 80), ("Burak", 140), ("Çağan", 50),
("Çağrı", 65), ("Deniz", 110), ("Doğan", 70), ("Doruk", 55),
("Emre", 160), ("Enes", 90), ("Eren", 130), ("Erhan", 75),
("Furkan", 85), ("Görkem", 60), ("Güven", 45), ("Haluk", 50),
("İlker", 70), ("Kaan", 95), ("Kadir", 80), ("Kerem", 100),
("Koral", 45), ("Korhan", 50), ("Mert", 130), ("Oğuz", 75),
("Onur", 90), ("Orkun", 55), ("Selim", 85), ("Sercan", 70),
("Serdar", 80), ("Soner", 65), ("Tarık", 75), ("Tuna", 50),
("Tunahan", 60), ("Uğur", 85), ("Umut", 100), ("Ufuk", 55),
("Volkan", 80), ("Yiğit", 90), ("Yunus", 95), ("Zafer", 60),
]
TURKCE_KADIN = [
("Aslı", 110), ("Aylin", 100), ("Aynur", 75), ("Ayşen", 65),
("Banu", 70), ("Bahar", 90), ("Başak", 80), ("Belgin", 55),
("Bengü", 50), ("Berrak", 60), ("Burcu", 95), ("Büşra", 85),
("Cansu", 100), ("Ceren", 120), ("Çiğdem", 80), ("Deniz", 90),
("Ebru", 95), ("Elçin", 65), ("Elif", 150), ("Esra", 110),
("Ezgi", 100), ("Gizem", 90), ("Gül", 75), ("Gülşen", 55),
("Güneş", 50), ("Hande", 85), ("İlayda", 75), ("İpek", 80),
("Melike", 90), ("Meltem", 95), ("Merve", 130), ("Nilay", 80),
("Nur", 70), ("Özge", 100), ("Pınar", 90), ("Seda", 85),
("Selin", 100), ("Sibel", 80), ("Simge", 75), ("Tuğba", 85),
("Tülay", 60), ("Ülkü", 50), ("Yasemin", 110), ("Zeynep", 120),
("Zümra", 55),
]
# Türkçe soyisimler
TURKCE_SOYISIM = [
("Yılmaz", 200), ("Kaya", 180), ("Demir", 170), ("Çelik", 150),
("Şahin", 140), ("Yıldız", 130), ("Arslan", 120), ("Doğan", 115),
("Kılıç", 110), ("Aslan", 105), ("Çetin", 100), ("Bulut", 95),
("Aydın", 90), ("Özdemir", 90), ("Demirci", 85), ("Güler", 80),
("Erdoğan", 75), ("Çakır", 75), ("Polat", 70), ("Koç", 70),
("Acar", 65), ("Kurt", 65), ("Yavuz", 65), ("Ateş", 60),
("Güneş", 60), ("ık", 60), ("Karaca", 55), ("Türk", 55),
("Özkan", 55), ("Bay", 50), ("Toker", 50), ("Şimşek", 50),
("Akay", 45), ("Boz", 45), ("Deniz", 45), ("Ercan", 45),
("Güçlü", 40), ("Kaplan", 40), ("Savaş", 40), ("Turan", 40),
("Baş", 35), ("Çam", 35), ("Kara", 35), ("Taş", 35),
("Dağ", 30), ("Duman", 30), ("Gür", 30), ("Köse", 30),
("Uçar", 30), ("Yurt", 30),
]
# -----------------------------------------------------------------------
# Batı kökenli isimler — ikinci öncelik
# -----------------------------------------------------------------------
BATI_ERKEK = [
("Can", 110), ("Cem", 90), ("Cenk", 75), ("Sarp", 60),
("Alper", 70), ("Enver", 55),
]
BATI_KADIN = [
("Ece", 100), ("Derya", 85), ("Sera", 60), ("Lara", 70),
("Nisa", 80), ("Sena", 75),
]
# -----------------------------------------------------------------------
# Arapça kökenli isimler — son öncelik (küçük liste)
# -----------------------------------------------------------------------
ARAPCA_ERKEK = [
("Ahmet", 160), ("Ali", 150), ("Mehmet", 170), ("Hasan", 100),
("Hüseyin", 95), ("İbrahim", 90), ("Mustafa", 140), ("Ömer", 80),
]
ARAPCA_KADIN = [
("Fatma", 120), ("Ayşe", 130), ("Hatice", 90), ("Havva", 70),
("Meryem", 80), ("Rabia", 65),
]
def _bulk_create(entries, name_type, gender, origin):
objs = []
for name, freq in entries:
objs.append(NameVocab(
name=name,
name_type=name_type,
gender=gender,
origin=origin,
frequency=freq,
))
# ignore_conflicts: aynı kayıt varsa atla
NameVocab.objects.bulk_create(objs, ignore_conflicts=True)
return len(objs)
class Command(BaseCommand):
help = 'NameVocab tablosunu Türkçe kökenli isimler öncelikli olarak doldurur'
def handle(self, *args, **options):
total = 0
# Türkçe — birinci öncelik
total += _bulk_create(TURKCE_ERKEK, 'first', 'E', 'turkce')
total += _bulk_create(TURKCE_KADIN, 'first', 'K', 'turkce')
total += _bulk_create(TURKCE_SOYISIM, 'last', 'U', 'turkce')
# Batı — ikinci öncelik
total += _bulk_create(BATI_ERKEK, 'first', 'E', 'bati')
total += _bulk_create(BATI_KADIN, 'first', 'K', 'bati')
# Arapça — son öncelik
total += _bulk_create(ARAPCA_ERKEK, 'first', 'E', 'arapca')
total += _bulk_create(ARAPCA_KADIN, 'first', 'K', 'arapca')
self.stdout.write(self.style.SUCCESS(
f'{total} isim işlendi. '
f'Türkçe: {NameVocab.objects.filter(origin="turkce").count()}, '
f'Batı: {NameVocab.objects.filter(origin="bati").count()}, '
f'Arapça: {NameVocab.objects.filter(origin="arapca").count()}'
))

View File

@@ -0,0 +1,40 @@
# Generated by Django 6.0.3 on 2026-03-27 19:47
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='TrainingJob',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('task_id', models.CharField(help_text='Celery task ID', max_length=255, unique=True)),
('status', models.CharField(choices=[('pending', 'Beklemede'), ('running', 'Eğitiliyor'), ('completed', 'Tamamlandı'), ('failed', 'Başarısız')], default='pending', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('started_at', models.DateTimeField(blank=True, null=True)),
('completed_at', models.DateTimeField(blank=True, null=True)),
('model_type', models.CharField(default='RandomForest', max_length=100)),
('model_version', models.DateTimeField(default=django.utils.timezone.now, help_text='Model versiyonu (timestamp)')),
('model_path', models.FilePathField(blank=True, null=True)),
('accuracy', models.FloatField(blank=True, null=True)),
('precision', models.FloatField(blank=True, null=True)),
('recall', models.FloatField(blank=True, null=True)),
('f1_score', models.FloatField(blank=True, null=True)),
('error_message', models.TextField(blank=True, null=True)),
('go_service_notified', models.BooleanField(default=False)),
],
options={
'verbose_name': 'Eğitim Görevi',
'verbose_name_plural': 'Eğitim Görevleri',
'ordering': ['-created_at'],
},
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 6.0.3 on 2026-03-27 19:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('namecreate', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='trainingjob',
name='feature_count',
field=models.PositiveIntegerField(blank=True, help_text='Özellik (sütun) sayısı — ONNX tipi için kullanılır', null=True),
),
migrations.AddField(
model_name='trainingjob',
name='features',
field=models.JSONField(blank=True, help_text='2D liste: her satır bir örnek, her sütun bir özellik', null=True),
),
migrations.AddField(
model_name='trainingjob',
name='labels',
field=models.JSONField(blank=True, help_text='1D liste: her örneğin sınıf etiketi', null=True),
),
migrations.AddField(
model_name='trainingjob',
name='sample_count',
field=models.PositiveIntegerField(blank=True, help_text='Eğitim verisi satır sayısı', null=True),
),
]

View File

@@ -0,0 +1,49 @@
# Generated by Django 6.0.3 on 2026-03-27 20:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('namecreate', '0002_trainingjob_feature_count_trainingjob_features_and_more'),
]
operations = [
migrations.CreateModel(
name='GeneratedPerson',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=100)),
('last_name', models.CharField(max_length=100)),
('birth_date', models.DateField()),
('gender', models.CharField(choices=[('E', 'Erkek'), ('K', 'Kadın')], max_length=1)),
('confidence', models.FloatField(blank=True, help_text='Modelin seçim güven skoru (0-1)', null=True)),
('generated_at', models.DateTimeField(auto_now_add=True)),
('training_job', models.ForeignKey(blank=True, help_text='Bu kişiyi üreten model versiyonu', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='generated_persons', to='namecreate.trainingjob')),
],
options={
'verbose_name': 'Üretilen Kişi',
'verbose_name_plural': 'Üretilen Kişiler',
'ordering': ['-generated_at'],
},
),
migrations.CreateModel(
name='NameVocab',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='İsim veya soyisim', max_length=100)),
('name_type', models.CharField(choices=[('first', 'İsim'), ('last', 'Soyisim')], max_length=5)),
('gender', models.CharField(choices=[('E', 'Erkek'), ('K', 'Kadın'), ('U', 'Unisex')], default='U', max_length=1)),
('origin', models.CharField(choices=[('turkce', 'Türkçe'), ('bati', 'Batı'), ('diger', 'Diğer'), ('arapca', 'Arapça')], default='turkce', help_text='Türkçe kökenli isimler varsayılan ve önceliklidir', max_length=10)),
('frequency', models.PositiveIntegerField(default=1, help_text='Veri setindeki görülme sıklığı — ağırlıklı seçimde kullanılır')),
],
options={
'verbose_name': 'İsim Sözlüğü',
'verbose_name_plural': 'İsim Sözlüğü',
'ordering': ['origin', '-frequency', 'name'],
'unique_together': {('name', 'name_type', 'gender')},
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.3 on 2026-03-27 20:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('namecreate', '0003_generatedperson_namevocab'),
]
operations = [
migrations.AlterField(
model_name='trainingjob',
name='model_path',
field=models.CharField(blank=True, max_length=500, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.3 on 2026-03-27 20:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('namecreate', '0004_alter_trainingjob_model_path'),
]
operations = [
migrations.AddField(
model_name='generatedperson',
name='username',
field=models.CharField(blank=True, db_index=True, max_length=150),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.3 on 2026-03-27 20:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('namecreate', '0005_generatedperson_username'),
]
operations = [
migrations.AddField(
model_name='generatedperson',
name='username_locked',
field=models.BooleanField(default=False, help_text='Aciksa username yeniden uretilmez (force ile degistirilebilir).'),
),
]

View File

179
namecreate/models.py Normal file
View File

@@ -0,0 +1,179 @@
import random
from django.conf import settings
from django.db import models
from django.utils import timezone
from .username_utils import build_unique_username
class NameVocab(models.Model):
"""Eğitim ve üretim için isim/soyisim sözlüğü. Türkçe kökenli isimler önceliklidir."""
ORIGIN_CHOICES = [
('turkce', 'Türkçe'), # Birinci öncelik
('bati', 'Batı'),
('diger', 'Diğer'),
('arapca', 'Arapça'), # Son öncelik
]
NAME_TYPE_CHOICES = [
('first', 'İsim'),
('last', 'Soyisim'),
]
GENDER_CHOICES = [
('E', 'Erkek'),
('K', 'Kadın'),
('U', 'Unisex'),
]
name = models.CharField(max_length=100, help_text='İsim veya soyisim')
name_type = models.CharField(max_length=5, choices=NAME_TYPE_CHOICES)
gender = models.CharField(max_length=1, choices=GENDER_CHOICES, default='U')
origin = models.CharField(
max_length=10,
choices=ORIGIN_CHOICES,
default='turkce',
help_text='Türkçe kökenli isimler varsayılan ve önceliklidir',
)
frequency = models.PositiveIntegerField(
default=1,
help_text='Veri setindeki görülme sıklığı — ağırlıklı seçimde kullanılır'
)
class Meta:
ordering = ['origin', '-frequency', 'name']
verbose_name = 'İsim Sözlüğü'
verbose_name_plural = 'İsim Sözlüğü'
unique_together = [('name', 'name_type', 'gender')]
def __str__(self):
return f"{self.name} ({self.get_name_type_display()}, {self.get_gender_display()}, {self.get_origin_display()})"
class GeneratedPerson(models.Model):
"""Modelin ürettiği kişi kaydı."""
GENDER_CHOICES = [
('E', 'Erkek'),
('K', 'Kadın'),
]
training_job = models.ForeignKey(
'TrainingJob',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='generated_persons',
help_text='Bu kişiyi üreten model versiyonu',
)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
username = models.CharField(max_length=150, blank=True, db_index=True)
username_locked = models.BooleanField(
default=False,
help_text='Aciksa username yeniden uretilmez (force ile degistirilebilir).'
)
birth_date = models.DateField()
gender = models.CharField(max_length=1, choices=GENDER_CHOICES)
confidence = models.FloatField(
null=True, blank=True,
help_text='Modelin seçim güven skoru (0-1)'
)
generated_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-generated_at']
verbose_name = 'Üretilen Kişi'
verbose_name_plural = 'Üretilen Kişiler'
def __str__(self):
return f"{self.first_name} {self.last_name} ({self.birth_date})"
def regenerate_username(self, used_usernames=None, force=False, save=True):
"""Kayit icin yeni bir username uretir. Mevcut username her zaman degisir."""
if self.username_locked and not force:
return self.username
if used_usernames is None:
used_usernames = set(
GeneratedPerson.objects.exclude(pk=self.pk).exclude(username='').values_list('username', flat=True)
)
# Mevcut username'i yasak listesine ekle ve rastgele sonek ile basla
# Boylece ayni base'e hic donulmez (ping-pong olmaz)
if self.username:
used_usernames.add(self.username)
suffix_min = getattr(settings, 'USERNAME_SUFFIX_MIN', 2)
suffix_max = getattr(settings, 'USERNAME_SUFFIX_MAX', 999)
self.username = build_unique_username(
self.first_name,
self.last_name,
self.birth_date,
used_usernames,
_force_suffix=random.randint(suffix_min, suffix_max),
)
if save:
self.save(update_fields=['username'])
return self.username
class TrainingJob(models.Model):
"""Makine öğrenme model eğitim görevinin kaydı."""
STATUS_CHOICES = [
('pending', 'Beklemede'),
('running', 'Eğitiliyor'),
('completed', 'Tamamlandı'),
('failed', 'Başarısız'),
]
# Temel bilgiler
task_id = models.CharField(max_length=255, unique=True, help_text='Celery task ID')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
created_at = models.DateTimeField(auto_now_add=True)
started_at = models.DateTimeField(null=True, blank=True)
completed_at = models.DateTimeField(null=True, blank=True)
# Model bilgileri
model_type = models.CharField(max_length=100, default='RandomForest')
model_version = models.DateTimeField(default=timezone.now, help_text='Model versiyonu (timestamp)')
model_path = models.CharField(max_length=500, null=True, blank=True)
# Eğitim verisi
features = models.JSONField(
null=True, blank=True,
help_text='2D liste: her satır bir örnek, her sütun bir özellik'
)
labels = models.JSONField(
null=True, blank=True,
help_text='1D liste: her örneğin sınıf etiketi'
)
feature_count = models.PositiveIntegerField(
null=True, blank=True,
help_text='Özellik (sütun) sayısı — ONNX tipi için kullanılır'
)
sample_count = models.PositiveIntegerField(
null=True, blank=True,
help_text='Eğitim verisi satır sayısı'
)
# Metrikler
accuracy = models.FloatField(null=True, blank=True)
precision = models.FloatField(null=True, blank=True)
recall = models.FloatField(null=True, blank=True)
f1_score = models.FloatField(null=True, blank=True)
# Hata handling
error_message = models.TextField(null=True, blank=True)
# Go servisine sinyal
go_service_notified = models.BooleanField(default=False)
class Meta:
ordering = ['-created_at']
verbose_name = 'Eğitim Görevi'
verbose_name_plural = 'Eğitim Görevleri'
def __str__(self):
return f"{self.model_type} - {self.status} - {self.created_at}"

129
namecreate/tasks.py Normal file
View File

@@ -0,0 +1,129 @@
import os
import requests
import numpy as np
from datetime import datetime
from celery import shared_task
from django.conf import settings
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
from namecreate.models import TrainingJob
def notify_go_service(model_path, metrics):
"""Go servisine model yüklenmiş olduğunu bildirir."""
try:
go_service_url = settings.GO_SERVICE_URL
if not go_service_url:
return False
payload = {
"model_path": model_path,
"metrics": metrics,
"timestamp": datetime.now().isoformat(),
}
response = requests.post(
f"{go_service_url}/reload-model",
json=payload,
timeout=10
)
return response.status_code == 200
except Exception as e:
print(f"Go servisi bildirimi başarısız: {str(e)}")
return False
@shared_task(name='namecreate.tasks.train_model_task')
def train_model_task(task_id):
"""
Makine öğrenme modelini arka planda eğitir ve ONNX olarak kaydeder.
"""
try:
job = TrainingJob.objects.get(task_id=task_id)
job.status = 'running'
job.started_at = datetime.now()
job.save(update_fields=['status', 'started_at'])
# 1. Veri Seti Yükleme
if job.features and job.labels:
# Kullanıcının gönderdiği veri
X = np.array(job.features, dtype=np.float32)
y = np.array(job.labels, dtype=np.int32)
else:
# Demo: Iris dataset
iris = load_iris()
X, y = iris.data.astype(np.float32), iris.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
feature_count = X.shape[1]
# 2. Model Eğitimi
model = RandomForestClassifier(n_estimators=10, random_state=42)
model.fit(X_train, y_train)
# 3. Metrikleri Hesapla
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred, average='weighted')
recall = recall_score(y_test, y_pred, average='weighted')
f1 = f1_score(y_test, y_pred, average='weighted')
# 4. ONNX Formatına Dönüştür (feature_count dinamik)
initial_type = [('float_input', FloatTensorType([None, feature_count]))]
onx = convert_sklearn(model, initial_types=initial_type)
# 5. Dosyaya Kaydet (Versiyonlu - Timestamp ile)
timestamp = job.model_version.strftime('%Y-%m-%d_%H-%M-%S')
model_filename = f"model_{timestamp}.onnx"
model_path = os.path.join(settings.MEDIA_ROOT, 'models', model_filename)
os.makedirs(os.path.dirname(model_path), exist_ok=True)
with open(model_path, "wb") as f:
f.write(onx.SerializeToString())
# 6. Go Servisine Bilder
metrics = {
'accuracy': float(accuracy),
'precision': float(precision),
'recall': float(recall),
'f1_score': float(f1),
}
go_notified = notify_go_service(model_path, metrics)
# 7. Veritabanına Kaydet
job.status = 'completed'
job.completed_at = datetime.now()
job.model_path = model_path
job.accuracy = accuracy
job.precision = precision
job.recall = recall
job.f1_score = f1
job.go_service_notified = go_notified
job.save()
return {
'status': 'success',
'task_id': task_id,
'model_path': model_path,
'go_service_notified': go_notified,
'metrics': metrics
}
except Exception as e:
job = TrainingJob.objects.get(task_id=task_id)
job.status = 'failed'
job.error_message = str(e)
job.save(update_fields=['status', 'error_message'])
return {
'status': 'error',
'task_id': task_id,
'error': str(e)
}

3
namecreate/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

8
namecreate/urls.py Normal file
View File

@@ -0,0 +1,8 @@
from django.urls import path
from . import views
urlpatterns = [
path('train-model/', views.train_and_export_model, name='train-model'),
path('training-status/', views.get_training_status, name='training-status'),
path('training-jobs/', views.list_training_jobs, name='training-jobs'),
]

View File

@@ -0,0 +1,60 @@
import re
def normalize_for_username(value):
"""Turkce karakterleri ASCII'ye cevirip username-safe hale getirir."""
tr_map = str.maketrans({
'c': 'c',
'C': 'c',
'g': 'g',
'G': 'g',
'i': 'i',
'I': 'i',
'o': 'o',
'O': 'o',
's': 's',
'S': 's',
'u': 'u',
'U': 'u',
'ç': 'c',
'Ç': 'c',
'ğ': 'g',
'Ğ': 'g',
'ı': 'i',
'İ': 'i',
'ö': 'o',
'Ö': 'o',
'ş': 's',
'Ş': 's',
'ü': 'u',
'Ü': 'u',
})
value = value.translate(tr_map).lower()
return re.sub(r'[^a-z0-9]+', '', value)
def build_unique_username(first_name, last_name, birth_date, used_usernames, _force_suffix=None):
"""
ad.soyadYY formatinda username uretir, cakisma olursa sonek ekler.
_force_suffix: verilirse bare base denenmez, bu sayidan itibaren baslar
(regeneration ping-pong onlemek icin kullanilir).
"""
first = normalize_for_username(first_name or '')
last = normalize_for_username(last_name or '')
yy = str(birth_date.year)[-2:]
base = f"{first}.{last}{yy}" if first and last else f"user{yy}"
if _force_suffix is not None:
# Yeniden uretim: bare base'i hic deneme, rastgele bir sonek ile basla
counter = _force_suffix
candidate = f"{base}{counter}"
else:
candidate = base
counter = 1
while candidate in used_usernames:
counter += 1
candidate = f"{base}{counter}"
used_usernames.add(candidate)
return candidate

241
namecreate/views.py Normal file
View File

@@ -0,0 +1,241 @@
import csv
import io
import json
import os
import uuid
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.authentication import SessionAuthentication
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import IsAdminUser
from rest_framework_simplejwt.authentication import JWTAuthentication
from namecreate.models import TrainingJob
from namecreate.tasks import train_model_task
def _parse_json_data(request):
"""
JSON body'den veriyi çeker.
Beklenen format:
{
"features": [[1.2, 3.4, 5.6, 7.8], [2.1, 4.3, 6.5, 8.7], ...],
"labels": [0, 1, 2, ...]
}
"""
body = json.loads(request.body)
features = body.get('features')
labels = body.get('labels')
if not features or not labels:
raise ValueError("'features' ve 'labels' alanları zorunludur.")
if len(features) != len(labels):
raise ValueError("'features' ve 'labels' eleman sayısı eşit olmalıdır.")
if len(features) < 10:
raise ValueError("En az 10 eğitim örneği gönderiniz.")
# Tip güvenliği: tüm değerler sayısal olmalı
for i, row in enumerate(features):
if not all(isinstance(v, (int, float)) for v in row):
raise ValueError(f"features[{i}] içinde sayısal olmayan değer var.")
if not all(isinstance(v, (int, float)) for v in labels):
raise ValueError("labels listesi sadece sayısal değer içermelidir.")
return features, [int(v) for v in labels]
def _parse_csv_data(file):
"""
CSV dosyasından veriyi çeker.
Beklenen format — son sütun label, geri kalanlar feature:
1.2,3.4,5.6,7.8,0
2.1,4.3,6.5,8.7,1
...
Header satırı varsa otomatik atlanır.
"""
content = file.read().decode('utf-8')
reader = csv.reader(io.StringIO(content))
features, labels = [], []
for lineno, row in enumerate(reader, start=1):
if not row:
continue
# Header satırını atla
try:
values = [float(v) for v in row]
except ValueError:
if lineno == 1:
continue # Başlık satırı
raise ValueError(f"CSV satır {lineno}: sayısal olmayan değer.")
if len(values) < 2:
raise ValueError(f"CSV satır {lineno}: en az 2 sütun (özellik + etiket) gerekli.")
features.append(values[:-1])
labels.append(int(values[-1]))
if len(features) < 10:
raise ValueError("En az 10 eğitim örneği gönderiniz.")
return features, labels
@csrf_exempt
def train_and_export_model(request):
"""
Modeli arka planda eğitmek için Celery task'ı başlatır.
Veri gönderme yöntemleri:
1) JSON body:
POST /api/v1/ml/train-model/
Content-Type: application/json
{
"features": [[1.2, 3.4, 5.6, 7.8], ...],
"labels": [0, 1, 2, ...]
}
2) CSV dosyası:
POST /api/v1/ml/train-model/
Content-Type: multipart/form-data
Form alanı: data=<csv_dosyası>
(Son sütun label, geri kalanlar feature)
3) Veri göndermezseniz yerleşik Iris demo verisi kullanılır.
"""
if request.method != 'POST':
return JsonResponse({'error': 'Sadece POST destekleniyor.'}, status=405)
try:
features, labels = None, None
source = 'demo'
# --- Yöntem 1: JSON body ---
ct = request.content_type or ''
if 'application/json' in ct and request.body:
features, labels = _parse_json_data(request)
source = 'json'
# --- Yöntem 2: CSV dosyası ---
elif 'data' in request.FILES:
features, labels = _parse_csv_data(request.FILES['data'])
source = 'csv'
# --- Yöntem 3: Demo (Iris) ---
# features ve labels None kalır, task default veriyi kullanır
feature_count = len(features[0]) if features else None
sample_count = len(features) if features else None
task_id = str(uuid.uuid4())
TrainingJob.objects.create(
task_id=task_id,
status='pending',
features=features,
labels=labels,
feature_count=feature_count,
sample_count=sample_count,
)
celery_task = train_model_task.delay(task_id)
return JsonResponse({
'status': 'queued',
'message': 'Model eğitim görevi başlatıldı.',
'task_id': task_id,
'celery_task_id': celery_task.id,
'data_source': source,
'sample_count': sample_count,
'feature_count': feature_count,
})
except (ValueError, KeyError) as e:
return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
except Exception as e:
return JsonResponse({'status': 'error', 'message': str(e)}, status=500)
def get_training_status(request):
"""Task'ın durumunu sorgular."""
task_id = request.GET.get('task_id')
if not task_id:
return JsonResponse({
"error": "task_id gerekli"
}, status=400)
try:
job = TrainingJob.objects.get(task_id=task_id)
response = {
"task_id": job.task_id,
"status": job.status,
"created_at": job.created_at.isoformat() if job.created_at else None,
"started_at": job.started_at.isoformat() if job.started_at else None,
"completed_at": job.completed_at.isoformat() if job.completed_at else None,
}
# Task tamamlandığında metrikleri ekle
if job.status == 'completed':
response.update({
"model_version": job.model_version.isoformat(),
"metrics": {
"accuracy": job.accuracy,
"precision": job.precision,
"recall": job.recall,
"f1_score": job.f1_score,
}
})
# Task başarısız olmuşsa hata mesajı ekle
if job.status == 'failed':
response["error_message"] = job.error_message
return JsonResponse(response)
except TrainingJob.DoesNotExist:
return JsonResponse({
"error": "Task bulunamadı"
}, status=404)
@api_view(['GET'])
@authentication_classes([SessionAuthentication, JWTAuthentication])
@permission_classes([IsAdminUser])
def list_training_jobs(request):
"""TrainingJob kayıtlarını listeler."""
status_filter = request.GET.get('status')
limit = request.GET.get('limit', '50')
try:
limit = max(1, min(int(limit), 200))
except ValueError:
return JsonResponse({"error": "limit sayısal olmalıdır"}, status=400)
jobs = TrainingJob.objects.all().order_by('-created_at')
if status_filter:
jobs = jobs.filter(status=status_filter)
items = []
for job in jobs[:limit]:
model_exists = bool(job.model_path and os.path.exists(job.model_path))
items.append({
'id': job.pk,
'task_id': job.task_id,
'status': job.status,
'model_type': job.model_type,
'created_at': job.created_at.isoformat() if job.created_at else None,
'started_at': job.started_at.isoformat() if job.started_at else None,
'completed_at': job.completed_at.isoformat() if job.completed_at else None,
'sample_count': job.sample_count,
'feature_count': job.feature_count,
'model_path': job.model_path,
'model_exists': model_exists,
'error_message': job.error_message,
})
return JsonResponse({
'count': len(items),
'results': items,
})