35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Pixevo Bouwen: Technische Uitdagingen Achter een AI-Beeldplatform

Pixevo Bouwen: Technische Uitdagingen Achter een AI-Beeldplatform

Roger Heykoop
case-studies, rails, ai
Hoe we een multi-model AI-beeldgeneratieplatform bouwden met Rails — workflow engines, real-time verwerking, en de architectuurbeslissingen die het werkend maakten.

Wanneer iemand “een kat in een ruimtepak op Mars” intypt in Pixevo en drie seconden later een fotorealistisch beeld terugkrijgt, denken ze niet na over wat er onder de motorkap gebeurt. Dat hoeft ook niet. Maar het platform bouwen dat dit mogelijk maakt? Dat vereiste het oplossen van problemen die we in vijftien jaar Rails-ontwikkeling niet eerder waren tegengekomen.

Pixevo is een AI-beeld- en videogeneratieplatform dat we van de grond af hebben gebouwd. Het integreert meer dan 50 AI-modellen, verwerkt dagelijks duizenden generaties, en levert features als visuele workflow builders, batchverwerking, en een marktplaats waar gebruikers hun creaties verkopen. Dit is wat we leerden tijdens het bouwen.

Het Multi-Model Probleem

De meeste AI-beeldplatforms kiezen één model en bouwen daaromheen. Midjourney gebruikt hun eigen model. DALL-E gebruikt dat van OpenAI. Wij wilden dat Pixevo elk groot model aanbiedt — Flux, Imagen, Stable Diffusion, Kling voor video, plus ons eigen Nano Banana Pro — en gebruikers naadloos laat wisselen.

De uitdaging is niet het aanroepen van verschillende API’s. Dat is gewoon HTTP. De uitdaging is dat elk model andere invoerformaten, resolutiebeperkingen, generatietijden, prijsstructuren en foutmodi heeft. Flux wil beeldverhoudingen. Stable Diffusion wil exacte pixelafmetingen. Sommige modellen accepteren negatieve prompts. Andere negeren ze. Responstijden variëren van twee seconden tot twee minuten.

We bouwden een model-adapterlaag die dit alles normaliseert achter een consistente interface:

class Generation::Orchestrator
  def generate(prompt:, model:, params: {})
    adapter = ModelRegistry.adapter_for(model)
    normalized = adapter.normalize_params(params)
    
    adapter.validate!(normalized)
    
    result = with_retry(adapter.retry_policy) do
      adapter.generate(prompt: prompt, **normalized)
    end
    
    PostProcessor.new(result, target: params[:output_format]).process
  end
end

Elke adapter handelt de eigenaardigheden van zijn model af. De orchestrator maakt het niet uit of hij met een lokaal GPU-cluster of een externe API praat. Nieuwe modellen worden toegevoegd door een adapter te schrijven — we hebben er sinds de lancering twaalf uitgebracht zonder de kerncode voor generatie aan te raken.

Real-Time Generatie Updates

Gebruikers verwachten hun beeld te zien verschijnen, niet dertig seconden naar een spinner te staren. Sommige modellen streamen tussenresultaten. Andere retourneren niets tot ze klaar zijn. We hadden een uniforme real-time ervaring nodig, ongeacht het model.

We gebruiken Action Cable met een generatie-specifiek kanaal dat zowel streaming- als polling-modellen afhandelt:

class GenerationChannel < ApplicationCable::Channel
  def subscribed
    stream_for current_user
  end

  def self.broadcast_progress(user, generation)
    broadcast_to(user, {
      id: generation.id,
      status: generation.status,
      progress: generation.progress_percentage,
      preview_url: generation.preview_url,
      final_url: generation.completed? ? generation.result_url : nil
    })
  end
end

Voor streaming-modellen pushen we voorbeeldframes zodra ze binnenkomen. Voor niet-streaming modellen pollt de achtergrondtaak de provider en zendt voortgangsschattingen uit op basis van historische timingdata. De frontend kent het verschil niet — het rendert gewoon wat er via de socket binnenkomt.

Het lastige deel was de voortgangsschatting. Een model doet er typisch twaalf seconden over, maar bij drukte op de provider kan het veertig worden. We volgen P50 en P95 generatietijden per model en gebruiken een logaritmische curve voor de voortgangsbalk. Die start snel, vertraagt, en bereikt nooit 100% tot het beeld daadwerkelijk klaar is. Niemand merkt het, maar het voelt responsief.

De Workflow Engine

Pixevo’s visuele workflow builder laat gebruikers AI-operaties ketenen: een beeld genereren, opschalen, een gezicht verwisselen, de achtergrond verwijderen, kleurcorrectie toepassen. Elk knooppunt in de workflow is een onafhankelijke operatie met eigen invoer, uitvoer en foutafhandeling.

Een betrouwbare DAG (Directed Acyclic Graph) uitvoeringsengine bouwen binnen een Rails-app was het meest complexe onderdeel. Elke workflowstap kan één tot zestig seconden duren. Stappen kunnen parallel draaien als ze niet van elkaar afhangen. Elke stap kan falen, en je moet individuele knooppunten kunnen herhalen zonder de hele pipeline opnieuw te draaien.

We modelleerden het als een state machine met persistente status:

class Workflow::Execution < ApplicationRecord
  has_many :node_executions, dependent: :destroy
  
  state_machine :status, initial: :pending do
    event :start do
      transition pending: :running
    end
    
    event :complete do
      transition running: :completed
    end
    
    event :fail do
      transition running: :failed
    end
  end

  def execute!
    start!
    ready_nodes.each { |node| WorkflowNodeJob.perform_later(self, node) }
  end

  def node_completed(node)
    node.dependents.each do |dependent|
      if dependent.dependencies_met?
        WorkflowNodeJob.perform_later(self, dependent)
      end
    end
    
    complete! if all_nodes_completed?
  end
end

Elke knooppuntuitvoering draait als een onafhankelijke achtergrondtaak. Wanneer een knooppunt klaar is, controleert het welke downstream knooppunten nu gedeblokkeerd zijn en plaatst ze in de wachtrij. Dit geeft ons automatische parallellisatie — als een workflow drie onafhankelijke takken heeft, draaien ze allemaal tegelijk.

De marktplaats voegt een extra dimensie toe. Gebruikers publiceren workflows als templates, en andere gebruikers kunnen ze installeren en aanpassen. We moesten de workflowdefinitie (het template) scheiden van de uitvoering (een specifieke run), versiebeheer afhandelen wanneer makers hun workflows updaten, en het economische model beheren waarbij makers credits verdienen met installaties.

Beeldverwerking op Schaal

Gegenereerde beelden hebben nabewerking nodig voordat ze bij gebruikers terechtkomen. We schalen voor thumbnails, strippen metadata voor privacy, converteren formaten, genereren blurhash placeholders voor lazy loading, en voeren kwaliteitscontroles uit. Een enkele generatie kan zes afgeleide beelden produceren.

Dit synchroon verwerken zou de responstijden om zeep helpen. We bouwden een pipeline met Active Storage en aangepaste analyzers:

class ImageAnalyzer::Generation < ActiveStorage::Analyzer
  def metadata
    {
      width: image.width,
      height: image.height,
      format: image.format,
      blurhash: generate_blurhash,
      nsfw_score: content_safety_check,
      quality_score: assess_quality
    }
  end
end

De content safety check verdient aandacht. Elk gegenereerd beeld doorloopt een moderatiepipeline voordat het zichtbaar is. Dit moet snel zijn (gebruikers wachten) en nauwkeurig (false positives frustreren gebruikers, false negatives creëren juridische blootstelling). We gebruiken een combinatie van veiligheidsinstellingen op modelniveau en onze eigen classificatielaag.

Provider-Storingen Elegant Afhandelen

Wanneer je afhankelijk bent van externe AI-modelproviders, gaat er van alles mis. Rate limits worden bereikt. Providers gaan offline voor onderhoud. GPU-clusters raken vol. Eén model kan een prompt afwijzen die een ander prima afhandelt.

Onze circuit breaker implementatie volgt foutpercentages per provider en routeert automatisch om:

class ProviderCircuitBreaker
  FAILURE_THRESHOLD = 5
  RESET_TIMEOUT = 30.seconds

  def call(provider, &block)
    if circuit_open?(provider)
      raise CircuitOpenError if no_fallback?(provider)
      return fallback_provider(provider).call(&block)
    end

    begin
      result = yield
      record_success(provider)
      result
    rescue ProviderError => e
      record_failure(provider)
      retry_with_fallback(provider, e, &block)
    end
  end
end

Wanneer de API van Flux fouten begint te geven, stoppen we met bombarderen en zetten het verzoek in de wachtrij voor later, of bieden de gebruiker een alternatief model aan. Dit gebeurt transparant — de gebruiker ziet “generatie duurt langer dan gebruikelijk” in plaats van een foutpagina.

Databaseontwerp voor Generaties

Pixevo slaat miljoenen generatie-records op, elk met metadata over de prompt, het model, parameters, timing en resultaten. De naïeve aanpak — één grote tabel — zou bezwijken onder de querydruk.

We partitioneren generaties per maand en gebruiken een combinatie van PostgreSQL’s JSONB-kolommen voor flexibele modelspecifieke parameters en geïndexeerde kolommen voor dingen die we frequent bevragen:

class CreateGenerations < ActiveRecord::Migration[8.0]
  def change
    create_table :generations, id: :uuid do |t|
      t.references :user, null: false
      t.string :model_key, null: false
      t.string :status, null: false, default: 'pending'
      t.text :prompt
      t.jsonb :params, default: {}
      t.jsonb :result_metadata, default: {}
      t.integer :generation_time_ms
      t.decimal :cost_credits, precision: 10, scale: 4
      
      t.timestamps
      t.index [:user_id, :created_at]
      t.index [:model_key, :status]
      t.index :created_at
    end
  end
end

De JSONB params-kolom slaat modelspecifieke instellingen op zonder schemawijzigingen wanneer we modellen toevoegen. LoRA-gewichten voor Stable Diffusion opslaan? Gaat in params. Beeldverhouding voor Flux? Params. Camera-instellingen voor de JSON-modus van Nano Banana Pro? Params. De database maakt het niet uit.

Wat We Anders Zouden Doen

Drie dingen vallen achteraf op.

Ten eerste hadden we de workflow engine vanaf dag één als een apart service moeten bouwen. Het groeide organisch binnen de monoliet, en het later extraheren was pijnlijk. De uitvoeringsstatus, de knooppuntgrafiek, de marktplaats — het is complex genoeg om een eigen bounded context te rechtvaardigen.

Ten tweede onderschatten we het belang van generatie-caching. Identieke prompts met identieke parameters produceren identieke resultaten bij deterministische modellen. We cachen nu agressief, maar een cachelaag retrofitten op een bestaande generatiepipeline is rommeliger dan het er vanaf het begin in bouwen.

Ten derde hadden we eerder in observability moeten investeren. Wanneer een generatie om 2 uur ‘s nachts faalt, moet je weten of het je code is, de provider, of de prompt van de gebruiker. We voegden gestructureerde logging, distributed tracing en providerspecifieke health dashboards toe — maar pas nadat we te veel ochtenden hadden besteed aan het debuggen van productie-issues door het lezen van ruwe logs.

De Stack

Voor de geïnteresseerden, de technologiekeuzes:

  • Backend: Ruby on Rails 8, PostgreSQL 16, Redis
  • Real-time: Action Cable met AnyCable voor productie
  • Achtergrondtaken: Solid Queue (gemigreerd van Sidekiq)
  • Frontend: Hotwire met Stimulus controllers voor de interactieve delen
  • Beeldverwerking: libvips via ImageProcessing gem
  • Deployment: Kamal op Hetzner (EU-datacenters voor GDPR-compliance)
  • CDN: Cloudflare met R2 voor beeldopslag

Rails lijkt misschien een ongewone keuze voor een AI-platform. Dat is het niet. De AI-modellen draaien elders — op GPU-clusters achter API’s. Onze taak is orchestratie, gebruikersbeheer, facturering, marktplaatslogica en real-time communicatie. Precies waar Rails in uitblinkt.

Afsluiting

Het bouwen van Pixevo leerde ons dat het moeilijke deel van een AI-product niet de AI is. Het is alles eromheen: de orchestratielaag die meerdere modellen als één platform laat aanvoelen, de real-time infrastructuur die generaties van dertig seconden instant laat voelen, de foutafhandeling die gebruikers blij houdt wanneer providers uitvallen, en het datamodel dat flexibel blijft terwijl het AI-landschap onder je verschuift.

Als je iets vergelijkbaars bouwt en over architectuur wilt praten, neem contact op. We hebben genoeg fouten gemaakt om je de dure te besparen.

#rails #ai #beeldgeneratie #architectuur #real-time #workflows #pixevo
R

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation