first commit
This commit is contained in:
0
namecreate/__init__.py
Normal file
0
namecreate/__init__.py
Normal file
250
namecreate/admin.py
Normal file
250
namecreate/admin.py
Normal 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> '
|
||||
'<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
5
namecreate/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class NamecreateConfig(AppConfig):
|
||||
name = 'namecreate'
|
||||
110
namecreate/llm_generator.py
Normal file
110
namecreate/llm_generator.py
Normal 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
|
||||
0
namecreate/management/__init__.py
Normal file
0
namecreate/management/__init__.py
Normal file
0
namecreate/management/commands/__init__.py
Normal file
0
namecreate/management/commands/__init__.py
Normal file
204
namecreate/management/commands/generate_persons.py
Normal file
204
namecreate/management/commands/generate_persons.py
Normal 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ş"
|
||||
)
|
||||
125
namecreate/management/commands/seed_name_vocab.py
Normal file
125
namecreate/management/commands/seed_name_vocab.py
Normal 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), ("Işı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()}'
|
||||
))
|
||||
40
namecreate/migrations/0001_initial.py
Normal file
40
namecreate/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
49
namecreate/migrations/0003_generatedperson_namevocab.py
Normal file
49
namecreate/migrations/0003_generatedperson_namevocab.py
Normal 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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
18
namecreate/migrations/0004_alter_trainingjob_model_path.py
Normal file
18
namecreate/migrations/0004_alter_trainingjob_model_path.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
namecreate/migrations/0005_generatedperson_username.py
Normal file
18
namecreate/migrations/0005_generatedperson_username.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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).'),
|
||||
),
|
||||
]
|
||||
0
namecreate/migrations/__init__.py
Normal file
0
namecreate/migrations/__init__.py
Normal file
179
namecreate/models.py
Normal file
179
namecreate/models.py
Normal 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
129
namecreate/tasks.py
Normal 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
3
namecreate/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
8
namecreate/urls.py
Normal file
8
namecreate/urls.py
Normal 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'),
|
||||
]
|
||||
60
namecreate/username_utils.py
Normal file
60
namecreate/username_utils.py
Normal 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
241
namecreate/views.py
Normal 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,
|
||||
})
|
||||
Reference in New Issue
Block a user