Eski fotoğrafların nerede çekildiğini bulan bir uygulama yaptık
Fotoğraflarını yapay zeka görüntü modellerine gönderen, konum bilgisi alan ve GPS koordinatlarını dosyaların içine yazan bir masaüstü uygulama yaptık.
Herkesin böyle bir klasörü vardır. Eski seyahatlerden yüzlerce fotoğraf, telefon yedekleri, rastgele indirmeler. Hepsinin adı IMG_4523.jpg ya da DSC_0091.CR2. GPS verisi yok, konum yok, metadata’da işe yarar bir şey yok. Bir fotoğrafın Prag’dan olduğunu belli belirsiz hatırlıyorsun ama hangisi? Hiçbir fikrin yok.
Google Photos bunu çözmeye çalışıyor ama her şeyi Google’ın sunucularına yüklemen gerekiyor. Apple Photos da benzer bir şey yapıyor, tabii Apple ekosistemindeysen. İkisi de veriyi kendi platformlarında kilitli tutuyor. Biz farklı bir şey istedik. Fotoğraflarına bakan, nerede çekildiğini anlayan ve bu bilgiyi dosyaların içine yazan bir uygulama. İşin bittiğinde GPS koordinatları EXIF verisinde oluyor. Dosyaların anlamlı isimlere sahip oluyor. Ve bunları okumak için herhangi bir uygulamaya ihtiyacın olmuyor.
Bunu gerçekten kim kullanıyor
Yıllardır düzenlenmemiş fotoğrafları olan ve sürekli “eninde sonunda bunları düzenlerim” diyen insanlar. RAW çeken ve kamerasında GPS olmayan fotoğrafçılar. Konum verisi gereken emlak fotoğrafları olan emlakçılar. Yerinde ürün fotoğrafı çekip GPS’in dosyaya gömülmesini isteyen eBay satıcıları. Bir klasör dolusu fotoğraf açıp “bu neredeydi?” diye soran herkes.
Ayrıca sadece konum etiketi almak için tüm fotoğraf kütüphanesini bulut hizmetine yüklemek istemeyen insanlar. Fotoğraflar analiz için bir yapay zeka sağlayıcısına gönderiliyor (Google, OpenAI veya Anthropic). Ama hiçbir yerde depolanmıyor. Dosyaların senin bilgisayarında kalıyor. Sonuçlar yerel bir veritabanında kalıyor.
Teknoloji yığını
Uygulama Electron tabanlı. Frontend’de React ve TypeScript var, stil için Tailwind CSS v4 kullanılıyor. Veri, better-sqlite3 üzerinden yerel bir SQLite veritabanında tutuluyor. GPS ve diğer metadata’yı dosyalara yazmak için exiftool-vendored kullanıyoruz. Mevcut EXIF verisini okumak için de exifr. Yapay zeka çağrıları Google Gemini, OpenAI ve Anthropic’in resmi SDK’ları üzerinden gidiyor. Geliştirme için electron-vite, yükleyici paketlemek için electron-builder kullanıyoruz.
Egzotik bir şey yok. Sıkıcı, bakımlı araçları bilerek seçtik. Electron bellek kullanımı yüzünden eleştiriliyor ama insanların ara sıra çalıştırdığı bir masaüstü fotoğraf aracı için yeterli. Tek bir kod tabanından Windows, macOS ve Linux için dağıtım yapmamızı sağladı. Platforma özel kod yazmadık.
En zor kısım: GPS koordinat ayrıştırma
Başka hiçbir şey yakınına bile gelemedi.
Sorun şu: üç farklı yapay zeka modeline “bu fotoğraf nerede çekildi?” diye soruyorsun ve üç tamamen farklı yanıt formatı alıyorsun. Gemini {lat: 33.49, lng: -111.93} verebilir. OpenAI {gps_coordinates: {latitude: "33.49 N"}} şeklinde iç içe koyabilir. Bazı modeller "33.49, -111.93" gibi virgülle ayrılmış bir string döndürür. Diğerleri "33°29'N" gibi DMS formatı kullanır. Birkaçı hiçbir sebep yokken her şeyi bir diziye sarar.
Sonunda önce doğrudan alanları deneyen (lat, latitude, lat_degrees), sonra dört farklı anahtar adı altında iç içe nesneleri kontrol eden, sonra virgülle ayrılmış stringleri ayrıştırmaya çalışan, sonra DMS notasyonunu işleyen bir normalleştirici yazdık. Tamamen yapay zeka modellerinin bir yanıt formatında anlaşamaması yüzünden var olan yaklaşık 50 satır savunmacı kod. Tam olarak ne istediğini söylesen bile.
En güzel hata falsy-sıfır problemiydi. Değerleri dönüştürmek için Number(val) || null kullanıyorduk. Harika çalışıyor, enlem 0 olduğunda hariç (ki bu ekvator). Number(0) 0 verir, bu da JavaScript’te falsy’dir. Yani 0 || null sana null verir. Ekvatordaki her GPS koordinatını sessizce siliyorduk. Bulmamız istediğimizden uzun sürdü.
// Önce (bozuk):
const lat = Number(val) || null // 0 null oluyor
// Sonra:
function toNum(v: unknown): number | null {
const n = Number(v)
return isNaN(n) ? null : n
}
Bir süre yakalayamadık çünkü test fotoğraflarımızın hiçbiri ekvatordan değildi. Sınır değerler için test durumu eklediğimizde bulduk. 0 null olarak dönüyordu.
Grid görünümünde analiz edilmiş 7 fotoğraf böyle görünüyor. Yeşil noktalar yapay zekanın konumu belirlediği anlamına geliyor. Kırmızı noktalar düşük güven anlamına geliyor.

Veri nasıl akıyor
Mimari basit:
- Kullanıcı bir klasör açıyor. Klasörü resim dosyaları için tarıyoruz ve SQLite’ta bir “batch” oluşturuyoruz. Her resim için bir satır.
- Kullanıcı Analiz Et’e tıklıyor. Her resim için dosyayı okuyoruz, base64 kodluyoruz ve kullanıcının seçtiği yapay zeka sağlayıcısına gönderiyoruz. Prompt konum, şehir, eyalet, ülke, GPS koordinatları, zaman tahmini ve güven seviyesi istiyor.
- Yapay zeka yanıtı JSON olarak dönüyor (genellikle). Tüm format farklılıklarını ele almak için normalleştiricimizden geçiriyoruz. Bir Zod şemasına karşı doğruluyoruz ve sonucu veritabanına kaydediyoruz. Model saçmalık döndürürse Zod yakalar ve görev başarısız olarak işaretlenir.
- Kullanıcı sonuçları inceliyor. Her birini onaylayabilir, reddedebilir veya düzenleyebilir. Uygulama karşılaştırma yapılabilsin diye orijinal EXIF verisini yapay zeka sonuçlarının yanında gösteriyor.

- Kullanıcı Değişiklikleri Uygula’ya tıklıyor. Her orijinal dosyayı bir çıktı klasörüne kopyalıyoruz. Kopyaya exiftool-vendored ile GPS koordinatlarını yazıyoruz. Sonra bir şablona göre yeniden adlandırıyoruz. Orijinaller asla değiştirilmiyor.
Analiz, eşzamanlılığı (aynı anda birden fazla resim analizi), başarısızlıkta yeniden denemeyi, duraklat/devam et’i ve maliyet sınırlarını yöneten bir kuyruk sistemi üzerinden çalışıyor. Her şey SQLite’ta takip ediliyor. Uygulama batch ortasında çökerse kaldığı yerden devam ediyor.
Electron’un süreç modeli biraz karmaşıklık ekliyor. Yapay zeka çağrıları ve dosya I/O main process’te gerçekleşiyor. Arayüz renderer process’te çalışıyor. Aralarındaki iletişim IPC handler’lar üzerinden gidiyor. Yaklaşık 40 IPC endpoint’imiz var. Bu çok, ama her biri tek bir iş yapıyor.
Gerçekte neler yapabiliyor
Çoklu sağlayıcı yapay zeka analizi
Uygulama Google Gemini, OpenAI ve Anthropic üzerinden 16’dan fazla modelle çalışıyor. Her sağlayıcının yapılandırılmış çıktı desteğiyle kendi SDK entegrasyonu var (Gemini responseSchema kullanıyor, OpenAI zodResponseFormat kullanıyor, Anthropic output_format kullanıyor). Bir yanıt geldiğinde, modelin kullandığı alan adlarını (location_name, locationName, specific_location, place) bizim standart formatımıza eşleyen bir normalleştiriciden geçiyor. Uygulamanın geri kalanı hangi modelin sonucu ürettiğini bilmiyor.
7 fotoğrafın hepsini Gemini 3 Flash ile çalıştırdıktan sonraki liste görünümü burada. Hepsi yüksek güven ile döndü. Toplam maliyet bir kuruşun altında.

Modeller arasındaki maliyet farkı çok büyük. Gemini 3 Flash resim başına yaklaşık $0.0004. GPT-5 yaklaşık $0.02. Bu 50 kat fark. 500 fotoğraflık bir batch için bu $0.20’ye karşı $10. Model seçici sadece bir tercih meselesi değil. Gerçek bir bütçe kararı.
Neredeyse her resim formatı için GPS yazma
EXIF verisi yazmak için piexifjs ile başladık ama sadece JPEG destekliyordu. Gerçek fotoğraf kütüphaneleriyle test edene kadar bu sorun değildi. İnsanların ekran görüntülerinden PNG’leri, iPhone’lardan HEIC’leri, tarayıcılardan TIFF’leri ve kameralardan RAW dosyaları var. Bu yüzden Phil Harvey’in ExifTool’unu saran exiftool-vendored’a geçtik. JPEG, PNG, TIFF, WebP, HEIC, HEIF, AVIF ve bir sürü RAW formata (DNG, CR2, CR3, NEF, ARW, ORF, RW2) GPS yazıyor.
Uygulama her zaman kopyaya yazıyor, asla orijinale değil. Bir çıktı klasörü oluşturuyor, her dosyayı oraya kopyalıyor, sonra kopyaya GPS verisini yazıyor. Yeniden adlandırma da açıksa kopya yeni ismi alıyor. Her resim için her şeyin uygulandığı tek bir çıktı dosyası.
Zaman kazandıran hata sınıflandırması
Bir API çağrısı başarısız olduğunda hataya bakıp dört kategoriden birine ayırıyoruz: auth (yanlış veya süresi dolmuş API anahtarı), rate-limit (çok fazla istek), network (bağlantı hatası, zaman aşımı, DNS sorunları) veya unknown. Bu önemli çünkü auth hataları asla yeniden denenmemeli. API anahtarın yanlışsa 3 kere denemek sadece 30 saniye boşa harcar. Rate-limit ve network hataları yeniden deneniyor çünkü genellikle geçiciler.
export function classifyApiError(error: unknown): ErrorCategory {
const msg = (error instanceof Error ? error.message : String(error))
.toLowerCase()
const status = (error as { status?: number })?.status ??
(error as { statusCode?: number })?.statusCode
if (status === 401 || status === 403 ||
msg.includes('api key') || msg.includes('unauthorized')) {
return 'auth'
}
if (status === 429 || msg.includes('rate limit') ||
msg.includes('quota')) {
return 'rate-limit'
}
if (msg.includes('econnrefused') || msg.includes('fetch failed') ||
msg.includes('etimedout') || msg.includes('socket hang up')) {
return 'network'
}
return 'unknown'
}
// Kuyrukta sınıflandırma yeniden deneme davranışını kontrol ediyor:
const shouldRetry =
category !== 'auth' &&
!currentRun.cancelRequested &&
!this.isRunCostExceeded(run) &&
task.attemptCount <= run.retryLimit
Bu bilim değil, kalıp eşleştirme. Ama yaygın durumları yakalıyor ve en sinir bozucu başarısızlık modunu engelliyor: uygulamanın yanlış bir API anahtarını tekrar tekrar denemesini izlemek.
İşini kaybetmeyen toplu işleme
Uygulamaya 500 fotoğraf atıp gidebilirsin. Birden fazla eşzamanlı worker ile işliyor (ayarlanabilir, varsayılan 3). İlerlemeyi SQLite’ta takip ediyor. API yanıtlarındaki gerçek token sayılarını kullanarak gerçek zamanlı maliyet raporluyor. Tahmin değil. $2.00’lık bir maliyet sınırı koyarsan uygulama o sınıra ulaştığında analizi durduruyor. Kalan resimleri atlanmış olarak işaretliyor.
Kuyruk sistemi çökmeye dayanıklı. Her görev durum değişikliği başka bir şey olmadan önce veritabanına yazılıyor. Electron çökerse, elektrik giderse veya uygulamayı zorla kapatırsan, sonraki açılışta yarıda kalmış çalışmalar tespit ediliyor ve kaldığı yerden devam ediliyor. Uygulama 201. resimde çöktü diye 200 tamamlanmış analizi kaybetmezsin.
Batch ortasında duraklatabilirsin de. Yanlış model seçtiğini fark edersen veya devam etmeden önce erken sonuçları kontrol etmek istersen faydalı. İlk geçişte bazı resimlerin güveni düşük dönerse otomatik yükseltme ayarlayabilirsin. Uygulama sadece o resimleri daha pahalı bir modelle tekrar çalıştırır.
Yan yana EXIF karşılaştırması
Detay görünümü exifr kullanarak dosyadan orijinal EXIF verisini çekiyor (kamera modeli, çekim tarihi, boyutlar ve mevcut GPS). Bunu yapay zeka analiz sonuçlarının yanında gösteriyor. Bu iki şekilde faydalı. Birincisi, fotoğrafta zaten GPS varsa yapay zekanın onunla aynı fikirde olup olmadığını görebilirsin. Koordinatlar çok farklıysa bu tehlike işareti. İkincisi, kamera modeli ve tarih yapay zekanın zaman tahminini doğrulamana yardımcı oluyor. EXIF “Canon EOS R5, Mart 2023” diyorsa ve yapay zeka “tahmini 2015-2018” diyorsa bir şeyler yanlış.
Yaptığımız ödünler

En büyüğü: BYOK (kendi API anahtarını getir). Kullanıcıların Google AI Studio’ya veya OpenAI’ın paneline gidip bir API anahtarı oluşturup ayarlarımıza yapıştırması gerekiyor. Bu gerçek bir sürtünme. Teknik olmayan kullanıcılar bunu yapmaz.
Ama alternatif kendi API proxy’mizi çalıştırmaktı. Bu bir backend kurmak, faturalandırma ayarlamak, kullanım ölçümüyle uğraşmak, kötüye kullanımı ele almak ve sunuculara para ödemek demek. Kimsenin kullanıp kullanmayacağından emin olmadığımız bir yan proje için bu çok fazla altyapı. BYOK sayesinde uygulama tamamen bağımsız. Bir binary gönderiyoruz, kullanıcılar çalıştırıyor, biz hiçbir şey yönetmiyoruz.
Yerel yapay zeka modellerini de kullanmamaya karar verdik. Gemma 4, LLaVA ve benzeri modeller temel görüntü anlama yapabiliyor. Ama konum belirleme için test ettiğimizde sonuçlar belirsizdi. “Burası bir plaja benziyor” demen, “Waikiki Beach, Honolulu, Hawaii” demen gerektiğinde işe yaramıyor. Bulut modelleri bu konuda gerçekten daha iyi çünkü internetin daha fazlasını görmüşler. Düzgün bir görüntü modelini yerel olarak çalıştırmanın donanım gereksinimleri, bu uygulamayı gerçekten isteyecek insanların çoğunu dışlardı.
Üçüncü ödün, sadece bir tane yerine ilk günden üç yapay zeka sağlayıcısını desteklemekti. Bu API entegrasyon işini üçe katladı. Dürüst olmak gerekirse kullanıcıların %90’ı en ucuz olduğu için Gemini kullanacak. Ama zaten bir OpenAI anahtarın varsa ve başka bir servise kaydolmak istemiyorsan seçenek olması önemli.
Ve işte çıktı. EXIF verisine GPS koordinatları gömülmüş, yeniden adlandırılmış dosyalar. Windows dosya özelliklerinde doğrulayabilirsin.

Farklı ne yapardık
Sadece Gemini ile çıkar, diğer sağlayıcıları talebe göre eklerdik. Üç entegrasyon üç kat iş, üç kat sınır durum, üç kat test demekti.
Fiyatlandırma konusunda da daha az gidip gelirdik. Ücretsiz mi, ücretli mi, freemium mu, açık kaynak mı diye çok uzun tartıştık. O zaman gerçek ürüne harcanabilirdi. Sonunda açık kaynak yaptık. Fazla düşünmeyi bıraktığımızda bu kararı vermek yaklaşık 30 saniye sürdü.
Teknik tarafta veritabanı şeması üzerine baştan daha fazla düşünürdük. Yeni özellikler geldikçe birkaç kez sütun ve migration ekledik (mesela başta olmayan konumlar için state alanı). Biraz daha esnek bir şemayla başlamak birkaç tur ALTER TABLE’dan kurtarırdı.