All Work

ARTifactify

Identifying Egyptian artifacts through your camera — AI vision, AR, audio guides, and a Gemini chatbot in one platform.

RoleFull-Stack Developer & AI Integration Lead
Year2025–2026
Duration~8 months
TeamTeam of ~5 (graduation project)
Status🟢 Live
ARTifactify — Artifact scanning interface

At a Glance

22Artifact Classes (YOLOv8)
640×640ONNX Input Resolution
18REST API Controllers
4Auth Methods Supported

The Problem

Museums struggle to make their collections engaging and accessible. Traditional museum apps are static brochures that require you to already know what you're looking at. Audio guides cost extra, are device-bound, and are only available in one or two languages. Egyptian cultural heritage is extraordinarily rich but chronically under-digitized in interactive form. Visitors had no way to identify an artifact by pointing a phone at it, no interactive way to ask questions and get instant contextual answers, and no access to 3D models without specialized desktop software.

The Solution

ARTifactify is a full-stack museum companion with three client surfaces — Flutter mobile, React web admin, and ASP.NET Core REST API. Users point their phone at any of 22 recognized Egyptian artifacts; the server's ONNX session pool identifies it in real time and returns metadata, location, and era. A Gemini-powered chatbot answers questions in the user's preferred language via text or voice. ElevenLabs synthesizes audio narration on demand. The entire AI inference pipeline runs server-side, so mobile clients need zero ML dependencies and the model can be updated without an app store release.

Architecture

Key Features

Screenshots

Code Highlight

Fire-and-Forget Scan Logging with Safe DI Scope Management
// After returning the detection result to the client, persist the scan
// record and upload the image to Cloudinary in the background —
// without blocking the HTTP response or hitting ObjectDisposedException.

_ = Task.Run(async () =>
    await UploadAndLogDetectionAsync(
        capturedBytes, capturedMime, capturedArtifactId,
        capturedUserId, capturedConfidence));

// ...

private async Task UploadAndLogDetectionAsync(
    byte[] imageBytes, string detectedMime,
    int artifactId, string userId, double confidence)
{
    // Create a completely independent DI scope — the controller's scoped
    // DbContext is disposed as soon as the HTTP response is sent.
    await using var scope = _scopeFactory.CreateAsyncScope();
    var db        = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
    var cloudinary = scope.ServiceProvider.GetRequiredService<ICloudinaryService>();

    try
    {
        var publicId = $"scan_{DateTime.UtcNow:yyyyMMdd_HHmmss}_{Guid.NewGuid():N[..8]}";
        using var stream = new MemoryStream(imageBytes);
        var imageUrl = await cloudinary.UploadImageAsync(stream, publicId, "scans");

        db.DetectionHistories.Add(new DetectionHistory
        {
            AppUserId       = userId,
            ArtifactId      = artifactId,
            ScanDate        = DateTime.UtcNow,
            ScannedImageUrl = imageUrl,
            Confidence      = confidence
        });
        await db.SaveChangesAsync();
    }
    catch (Exception ex)
    {
        // Never let background failures surface to the user
        _logger.LogError(ex,
            "Failed to persist DetectionHistory for UserId: {UserId}, ArtifactId: {ArtifactId}",
            userId, artifactId);
    }
}

Challenges & Solutions

🎯 YOLOv8 ONNX model (~30 MB) causing 10–15 second cold-start on the first scan request

Implemented a `YoloModelLoader` singleton with a bounded `Channel<InferenceSession>` acting as a session pool (size 2). A hosted `ModelLoadingBackgroundService` pre-warms the pool on server startup asynchronously, so by the time the first real request arrives the model is already loaded. The singleton lives for the process lifetime, eliminating repeated file reads and ONNX session creation overhead.

🎯 ObjectDisposedException on scoped DbContext in background scan-history task

Used `IServiceScopeFactory.CreateAsyncScope()` inside `Task.Run(...)` to create a completely independent DI scope owned by the background task. All database and Cloudinary operations run in this fresh scope, ensuring the DbContext is alive for exactly as long as the background work needs it — then properly disposed via `await using`.

🎯 HuggingFace STT service cold-start of up to 120 seconds causing Polly to fire too early

Exposed `HuggingFaceChatbot:TimeoutSeconds` in `appsettings.json` (default 180s) and dynamically computed all Polly resilience parameters from it — `TotalRequestTimeout = timeout + 20s`, `AttemptTimeout = timeout`, `CircuitBreaker.SamplingDuration = timeout × 2 + 60s` (satisfying the framework invariant `SamplingDuration ≥ 2 × AttemptTimeout`). Retry count reduced to 1 because audio streams are not idempotent.

Tech Stack

ASP.NET Core 10 Web APIC# / .NET 10CQRS / MediatRYOLOv8 (ONNX Runtime)Gemini AI (chatbot)ElevenLabs TTSFlutter / DartReact 19 (admin)Entity Framework CoreSQL ServerStripeCloudinaryPolly (resilience)

What I Learned

Links

Interested in a similar solution?

Let's discuss how I can build something like this for your business.