diff --git a/app/apps/core/forms.py b/app/apps/core/forms.py
index fe2297165a55961d4cd1425e5b6f17615e6037e5..4d9ea04b6f1802c7b0be09639897413746885057 100644
--- a/app/apps/core/forms.py
+++ b/app/apps/core/forms.py
@@ -5,6 +5,7 @@ from PIL import Image
 from django import forms
 from django.conf import settings
 from django.core.validators import FileExtensionValidator, MinValueValidator, MaxValueValidator
+from django.core.files.uploadedfile import TemporaryUploadedFile
 from django.db.models import Q
 from django.forms.models import inlineformset_factory
 from django.utils import timezone
@@ -17,6 +18,8 @@ from core.models import (Project, Document, Metadata, DocumentMetadata,
 
                          BlockType, LineType, AlreadyProcessingException)
 from users.models import User
+from kraken.lib import vgsl
+from kraken.lib.exceptions import KrakenInvalidModelException
 
 logger = logging.getLogger(__name__)
 
@@ -122,6 +125,42 @@ MetadataFormSet = inlineformset_factory(Document, DocumentMetadata,
                                         extra=1, can_delete=True)
 
 
+class ModelUploadForm(BootstrapFormMixin, forms.ModelForm):
+    name = forms.CharField()
+    file = forms.FileField(
+        validators=[FileExtensionValidator(
+        allowed_extensions=['mlmodel'])]
+    )
+
+    def clean_file(self):
+        # Early validation of the model loading
+        file_field = self.cleaned_data['file']
+        try:
+            model = vgsl.TorchVGSLModel.load_model(file_field.file.name)
+        except KrakenInvalidModelException:
+            raise forms.ValidationError(_("The provided model could not be loaded."))
+        self._model_job = model.model_type
+        if self._model_job not in ('segmentation', 'recognition'):
+            raise forms.ValidationError(_("Invalid model (Couldn't determine whether it's a segmentation or recognition model)."))
+        elif self._model_job == 'recognition' and model.seg_type == "bbox":
+            raise forms.ValidationError(_("eScriptorium is not compatible with bounding box models."))
+        return file_field
+
+    def clean(self):
+        if not getattr(self, '_model_job', None):
+            return super().clean()
+        # Update the job field on the instantiated model from the cleaned model field
+        if self._model_job == 'segmentation':
+            self.instance.job = OcrModel.MODEL_JOB_SEGMENT
+        elif self._model_job == 'recognition':
+            self.instance.job = OcrModel.MODEL_JOB_RECOGNIZE
+        return super().clean()
+
+    class Meta:
+        model = OcrModel
+        fields = ('name', 'file')
+
+
 class DocumentProcessForm1(BootstrapFormMixin, forms.Form):
     parts = forms.CharField()
 
@@ -142,12 +181,14 @@ class DocumentProcessForm1(BootstrapFormMixin, forms.Form):
         if self.document.read_direction == self.document.READ_DIRECTION_RTL:
             self.initial['text_direction'] = 'horizontal-rl'
         self.fields['binarizer'].widget.attrs['disabled'] = True
-        self.fields['train_model'].queryset &= self.document.ocr_models.all()
-        self.fields['segtrain_model'].queryset &= self.document.ocr_models.all()
-        self.fields['seg_model'].queryset &= self.document.ocr_models.all()
-        self.fields['ocr_model'].queryset &= OcrModel.objects.filter(
-            Q(documents=None, script=self.document.main_script)
-            | Q(documents=self.document))
+
+        # Limit querysets to models owned by the user or already linked to this document
+        for field in ['train_model', 'segtrain_model', 'seg_model', 'ocr_model']:
+            self.fields[field].queryset = self.fields[field].queryset.filter(
+                Q(owner=self.user)
+                | Q(documents=self.document)
+            )
+
         self.fields['transcription'].queryset = Transcription.objects.filter(document=self.document)
 
     def process(self):
@@ -176,29 +217,12 @@ class DocumentSegmentForm(DocumentProcessForm1):
                               ('vertical-rl', _("Vertical r2l")))
     text_direction = forms.ChoiceField(initial='horizontal-lr', required=False,
                                        choices=TEXT_DIRECTION_CHOICES)
-    upload_model = forms.FileField(required=False,
-                                   validators=[FileExtensionValidator(
-                                       allowed_extensions=['mlmodel', 'pronn', 'clstm'])])
 
     def clean(self):
         data = super().clean()
         model_job = OcrModel.MODEL_JOB_SEGMENT
 
-        if data.get('upload_model'):
-            model = OcrModel.objects.create(
-                owner=self.user,
-                name=data['upload_model'].name.rsplit('.', 1)[0],
-                job=model_job)
-            OcrModelDocument.objects.create(
-                document=self.parts[0].document,
-                ocr_model=model,
-                executed_on=timezone.now(),
-            )
-            # Note: needs to save the file in a second step because the path needs the db PK
-            model.file = data['upload_model']
-            model.save()
-
-        elif data.get('seg_model'):
+        if data.get('seg_model'):
             model = data.get('seg_model')
             ocr_model_document, created = OcrModelDocument.objects.get_or_create(
                 ocr_model=model,
@@ -231,9 +255,6 @@ class DocumentTrainForm(DocumentProcessForm1):
     train_model = forms.ModelChoiceField(queryset=OcrModel.objects
                                          .filter(job=OcrModel.MODEL_JOB_RECOGNIZE),
                                          label=_("Model"), required=False)
-    upload_model = forms.FileField(required=False,
-                                   validators=[FileExtensionValidator(
-                                       allowed_extensions=['mlmodel', 'pronn', 'clstm'])])
 
     transcription = forms.ModelChoiceField(queryset=Transcription.objects.all(), required=False)
 
@@ -259,20 +280,6 @@ class DocumentTrainForm(DocumentProcessForm1):
                 ocr_model_document.trained_on = timezone.now()
                 ocr_model_document.save()
 
-        elif data.get('upload_model'):
-            model = OcrModel.objects.create(
-                owner=self.user,
-                name=data['upload_model'].name.rsplit('.', 1)[0],
-                job=model_job)
-            OcrModelDocument.objects.create(
-                document=self.parts[0].document,
-                ocr_model=model,
-                trained_on=timezone.now(),
-            )
-            # Note: needs to save the file in a second step because the path needs the db PK
-            model.file = data['upload_model']
-            model.save()
-
         elif data.get('new_model'):
             # file will be created by the training process
             model = OcrModel.objects.create(
@@ -305,10 +312,6 @@ class DocumentSegtrainForm(DocumentProcessForm1):
     segtrain_model = forms.ModelChoiceField(queryset=OcrModel.objects
                                             .filter(job=OcrModel.MODEL_JOB_SEGMENT),
                                             label=_("Model"), required=False)
-    upload_model = forms.FileField(required=False,
-                                   validators=[FileExtensionValidator(
-                                       allowed_extensions=['mlmodel', 'pronn', 'clstm'])])
-
     new_model = forms.CharField(required=False, label=_('Model name'))
 
     def clean(self):
@@ -329,19 +332,6 @@ class DocumentSegtrainForm(DocumentProcessForm1):
             if not created:
                 ocr_model_document.trained_on = timezone.now()
                 ocr_model_document.save()
-        elif data.get('upload_model'):
-            model = OcrModel.objects.create(
-                owner=self.user,
-                name=data['upload_model'].name.rsplit('.', 1)[0],
-                job=model_job)
-            OcrModelDocument.objects.create(
-                document=self.parts[0].document,
-                ocr_model=model,
-                trained_on=timezone.now(),
-            )
-            # Note: needs to save the file in a second step because the path needs the db PK
-            model.file = data['upload_model']
-            model.save()
 
         elif data.get('new_model'):
             # file will be created by the training process
@@ -372,9 +362,6 @@ class DocumentSegtrainForm(DocumentProcessForm1):
 
 class DocumentTranscribeForm(DocumentProcessForm1):
 
-    upload_model = forms.FileField(required=False,
-                                   validators=[FileExtensionValidator(
-                                       allowed_extensions=['mlmodel', 'pronn', 'clstm'])])
     ocr_model = forms.ModelChoiceField(queryset=OcrModel.objects
                                        .filter(job=OcrModel.MODEL_JOB_RECOGNIZE),
                                        label=_("Model"), required=False)
@@ -384,33 +371,15 @@ class DocumentTranscribeForm(DocumentProcessForm1):
 
         model_job = OcrModel.MODEL_JOB_RECOGNIZE
 
-        if data.get('upload_model'):
-            model = OcrModel.objects.create(
-                owner=self.user,
-                name=data['upload_model'].name.rsplit('.', 1)[0],
-                job=model_job)
-            OcrModelDocument.objects.create(
-                document=self.parts[0].document,
-                ocr_model=model,
-                executed_on=timezone.now(),
-            )
-            # Note: needs to save the file in a second step because the path needs the db PK
-            model.file = data['upload_model']
-            model.save()
-
-        elif data.get('ocr_model'):
-            model = data.get('ocr_model')
-            ocr_model_document, created = OcrModelDocument.objects.get_or_create(
-                ocr_model=model,
-                document=self.parts[0].document,
-                defaults={'executed_on': timezone.now()}
-            )
-            if not created:
-                ocr_model_document.executed_on = timezone.now()
-                ocr_model_document.save()
-        else:
-            raise forms.ValidationError(
-                    _("Either select a name for your new model or an existing one."))
+        model = data['ocr_model']
+        ocr_model_document, created = OcrModelDocument.objects.get_or_create(
+            ocr_model=model,
+            document=self.parts[0].document,
+            defaults={'executed_on': timezone.now()}
+        )
+        if not created:
+            ocr_model_document.executed_on = timezone.now()
+            ocr_model_document.save()
 
         data['model'] = model
         return data
@@ -474,9 +443,6 @@ class DocumentProcessForm(BootstrapFormMixin, forms.Form):
     text_direction = forms.ChoiceField(initial='horizontal-lr', required=False,
                                        choices=TEXT_DIRECTION_CHOICES)
     # transcribe
-    upload_model = forms.FileField(required=False,
-                                   validators=[FileExtensionValidator(
-                                       allowed_extensions=['mlmodel', 'pronn', 'clstm'])])
     ocr_model = forms.ModelChoiceField(queryset=OcrModel.objects
                                        .filter(job=OcrModel.MODEL_JOB_RECOGNIZE),
                                        label=_("Model"), required=False)
@@ -506,12 +472,14 @@ class DocumentProcessForm(BootstrapFormMixin, forms.Form):
         if self.document.read_direction == self.document.READ_DIRECTION_RTL:
             self.initial['text_direction'] = 'horizontal-rl'
         self.fields['binarizer'].widget.attrs['disabled'] = True
-        self.fields['train_model'].queryset &= self.document.ocr_models.all()
-        self.fields['segtrain_model'].queryset &= self.document.ocr_models.all()
-        self.fields['seg_model'].queryset &= self.document.ocr_models.all()
-        self.fields['ocr_model'].queryset &= OcrModel.objects.filter(
-            Q(documents=None, script=self.document.main_script)
-            | Q(documents=self.document))
+
+        # Limit querysets to models owned by the user or already linked to this document
+        for field in ['train_model', 'segtrain_model', 'seg_model', 'ocr_model']:
+            self.fields[field].queryset = self.fields[field].queryset.filter(
+                Q(owner=self.user)
+                | Q(documents=self.document)
+            )
+
         self.fields['transcription'].queryset = Transcription.objects.filter(document=self.document)
 
     @cached_property
@@ -575,19 +543,6 @@ class DocumentProcessForm(BootstrapFormMixin, forms.Form):
             if not created:
                 ocr_model_document.trained_on = timezone.now()
                 ocr_model_document.save()
-        elif data.get('upload_model'):
-            model = OcrModel.objects.create(
-                owner=self.user,
-                name=data['upload_model'].name.rsplit('.', 1)[0],
-                job=model_job)
-            OcrModelDocument.objects.create(
-                document=self.parts[0].document,
-                ocr_model=model,
-                executed_on=timezone.now(),
-            )
-            # Note: needs to save the file in a second step because the path needs the db PK
-            model.file = data['upload_model']
-            model.save()
 
         elif data.get('new_model'):
             # file will be created by the training process
diff --git a/app/apps/core/urls.py b/app/apps/core/urls.py
index 2420d62545807b9cf2eef5a9efe52d053ed0a1eb..1480cc68ab1f4d02e398cf28e8f541cfcfbe80c5 100644
--- a/app/apps/core/urls.py
+++ b/app/apps/core/urls.py
@@ -10,12 +10,14 @@ from core.views import (Home,
                         UpdateDocument,
                         EditPart,
                         DocumentImages,
-                        ModelsList,
+                        UserModels,
+                        DocumentModels,
                         ModelDelete,
                         ModelCancelTraining,
                         PublishDocument,
                         ShareProject,
-                        DocumentPartsProcessAjax)
+                        DocumentPartsProcessAjax,
+                        ModelUpload)
 
 urlpatterns = [
     path('', Home.as_view(), name='home'),
@@ -33,11 +35,12 @@ urlpatterns = [
     path('document/<int:pk>/part/<int:part_pk>/edit/', EditPart.as_view(),
          name='document-part-edit'),
     path('document/<int:pk>/images/', DocumentImages.as_view(), name='document-images'),
-    path('models/', ModelsList.as_view(), name='user-models'),
+    path('models/', UserModels.as_view(), name='user-models'),
+    path('models/new/', ModelUpload.as_view(), name='model-upload'),
     path('model/<int:pk>/delete/', ModelDelete.as_view(), name='model-delete'),
     path('model/<int:pk>/cancel_training/', ModelCancelTraining.as_view(),
          name='model-cancel-training'),
-    path('document/<int:document_pk>/models/', ModelsList.as_view(), name='document-models'),
+    path('document/<int:document_pk>/models/', DocumentModels.as_view(), name='document-models'),
     path('document/<int:pk>/publish/', PublishDocument.as_view(), name='document-publish'),
     path('document/<int:pk>/process/', DocumentPartsProcessAjax.as_view(),
          name='document-parts-process'),
diff --git a/app/apps/core/views.py b/app/apps/core/views.py
index d8cfd458e7ce1edf8f7b27949464e1f0a9cf1fd6..efbb3fb35c8bac7564070c1bbc2d2e04238d6280 100644
--- a/app/apps/core/views.py
+++ b/app/apps/core/views.py
@@ -16,7 +16,7 @@ from django.views.generic import CreateView, UpdateView, DeleteView
 from core.models import (Project, Document, DocumentPart, Metadata,
                          OcrModel, AlreadyProcessingException)
 from core.forms import (ProjectForm, DocumentForm, MetadataFormSet, ProjectShareForm,
-                        UploadImageForm, DocumentProcessForm)
+                        UploadImageForm, DocumentProcessForm, ModelUploadForm)
 from imports.forms import ImportForm, ExportForm
 
 
@@ -296,30 +296,49 @@ class EditPart(LoginRequiredMixin, DetailView):
             return super().dispatch(*args, **kwargs)
 
 
-class ModelsList(LoginRequiredMixin, ListView):
+class DocumentModels(LoginRequiredMixin, ListView):
     model = OcrModel
-    template_name = "core/models_list.html"
+    template_name = "core/models_list/document_models.html"
     http_method_names = ('get',)
+    paginate_by = 20
 
     def get_queryset(self):
-        if 'document_pk' in self.kwargs:
-            try:
-                self.document = Document.objects.for_user(self.request.user).get(pk=self.kwargs.get('document_pk'))
-            except Document.DoesNotExist:
-                raise PermissionDenied
-            return self.document.ocr_models.all()
-        else:
-            self.document = None
-            return OcrModel.objects.filter(owner=self.request.user)
+        try:
+            self.document = Document.objects.for_user(self.request.user).get(pk=self.kwargs.get('document_pk'))
+        except Document.DoesNotExist:
+            raise PermissionDenied
+        return self.document.ocr_models.all()
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
-        if self.document:
-            context['document'] = self.document
-            context['object'] = self.document  # legacy
+        context['document'] = self.document
+        context['object'] = self.document  # legacy
         return context
 
 
+class UserModels(LoginRequiredMixin, ListView):
+    model = OcrModel
+    template_name = "core/models_list/main.html"
+    http_method_names = ('get',)
+    paginate_by = 20
+
+    def get_queryset(self):
+        return OcrModel.objects.filter(owner=self.request.user)
+
+
+class ModelUpload(LoginRequiredMixin, SuccessMessageMixin, CreateView):
+    model = OcrModel
+    form_class = ModelUploadForm
+    success_message = _("Model uploaded successfully!")
+
+    def get_success_url(self):
+        return reverse('user-models')
+
+    def form_valid(self, form):
+        form.instance.owner = self.request.user
+        return super().form_valid(form)
+
+
 class ModelDelete(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
     model = OcrModel
     success_message = _("Model deleted successfully!")
diff --git a/app/escriptorium/templates/base.html b/app/escriptorium/templates/base.html
index e88a8886e2fab5c74e9993b6bf7732b6b9c89313..dbd1cd3816ff89cdff9da742500bb3e407fdc68f 100644
--- a/app/escriptorium/templates/base.html
+++ b/app/escriptorium/templates/base.html
@@ -65,6 +65,9 @@
           <li class="nav-item">
             <a class="nav-link {% block nav-proj-list-class %}{% endblock %}" href="{% url 'projects-list' %}">{% trans "My Projects" %}</a>
           </li>
+          <li class="nav-item">
+            <a class="nav-link" href="{% url 'user-models' %}">{% trans "My models" %}</a>
+          </li>
           <li class="nav-item dropdown">
               <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                   {% blocktrans with username=user.username %}Hello {{username}}{% endblocktrans %}
diff --git a/app/escriptorium/templates/core/document_nav.html b/app/escriptorium/templates/core/document_nav.html
index a0dea118aaca16d763f3d92ca547ad3d743206d3..5741aadc420cc9f5a94d754619bd8bb09d930641 100644
--- a/app/escriptorium/templates/core/document_nav.html
+++ b/app/escriptorium/templates/core/document_nav.html
@@ -9,11 +9,9 @@
       <a href="{% if object %}{% url 'document-update' pk=document.pk %}{% endif %}" class="nav-item nav-link {% block nav-doc-active %}{% endblock %}" id="nav-doc-tab" role="tab" aria-controls="nav-doc" aria-selected="true">{% trans "Description" %}</a>
       <a href="{% if object %}{% url 'document-images' pk=document.pk %}{% else %}#{% endif %}" class="nav-item nav-link {% if not object %}disabled{% endif %} {% block nav-images-active %}{% endblock %}" id="nav-img-tab" role="tab" aria-controls="nav-img" aria-selected="true">{% trans "Images" %}</a>
       <a href="{% if document  %}{% url 'document-part-edit' pk=document.pk %}{% else %}#{% endif %}" class="nav-item nav-link {% block nav-edit-active %}{% endblock %}{% if not object or not document.parts.count %}disabled{% endif %}" id="nav-edit-tab" role="tab" aria-controls="nav-doc" aria-selected="true">{% trans "Edit" %}</a>
-      {% if perms.core.can_train %}
       {% with models_count=document.ocr_models.count %}
       <a href="{% if document and models_count %}{% url 'document-models' document_pk=document.pk %}{% else %}#{% endif %}" class="nav-item nav-link {% if not document or not models_count %}disabled{% endif %}" id="nav-models-tab" role="tab" aria-controls="nav-doc" aria-selected="true">{% trans "Models" %}</a>
       {% endwith %}
-      {% endif %}
       {% block extra_nav %}<div class="nav-div nav-item ml-5 ">{{document.name}}</div>{% endblock %}
     </div>
   </nav>
diff --git a/app/escriptorium/templates/core/document_part_edit.html b/app/escriptorium/templates/core/document_part_edit.html
index 0cb1a7128de5f7ab355736a8fe435217f81bc1af..0c6d857774b86e3eb17907cb11366d9fe0261f87 100644
--- a/app/escriptorium/templates/core/document_part_edit.html
+++ b/app/escriptorium/templates/core/document_part_edit.html
@@ -13,11 +13,9 @@
     <a href="{% if object %}{% url 'document-update' pk=document.pk %}{% endif %}" class="nav-item nav-link {% block nav-doc-active %}{% endblock %}" id="nav-doc-tab" role="tab" aria-controls="nav-doc" aria-selected="true">{% trans "Description" %}</a>
     <a href="{% if object %}{% url 'document-images' pk=document.pk %}{% else %}#{% endif %}" class="nav-item nav-link {% if not object %}disabled{% endif %} {% block nav-images-active %}{% endblock %}" id="nav-img-tab" role="tab" aria-controls="nav-img" aria-selected="true">{% trans "Images" %}</a>
     <a href="{% if document  %}{% url 'document-part-edit' pk=document.pk %}{% else %}#{% endif %}" class="nav-item nav-link active {% if not object or not document.parts.count %}disabled{% endif %}" id="nav-edit-tab" role="tab" aria-controls="nav-doc" aria-selected="true">{% trans "Edit" %}</a>
-    {% if perms.core.can_train %}
     {% with models_count=document.ocr_models.count %}
     <a href="{% if document and models_count %}{% url 'document-models' document_pk=document.pk %}{% else %}#{% endif %}" class="nav-item nav-link {% if not document or not models_count %}disabled{% endif %}" id="nav-models-tab" role="tab" aria-controls="nav-doc" aria-selected="true">{% trans "Models" %}</a>
     {% endwith %}
-    {% endif %}
   </editor>
 </div>
 {% endblock %}
diff --git a/app/escriptorium/templates/core/models_list/document_models.html b/app/escriptorium/templates/core/models_list/document_models.html
new file mode 100644
index 0000000000000000000000000000000000000000..07adfa0f61443a12cad1b204b88b379faf6cd970
--- /dev/null
+++ b/app/escriptorium/templates/core/models_list/document_models.html
@@ -0,0 +1,9 @@
+{% extends 'core/document_nav.html' %}
+{% load i18n staticfiles %}
+
+{% block tab_content %}
+    {% include 'core/models_list/table.html' %}
+{% endblock %}
+{% block scripts %}
+    {% include 'core/models_list/scripts.html' %}
+{% endblock %}
diff --git a/app/escriptorium/templates/core/models_list/main.html b/app/escriptorium/templates/core/models_list/main.html
new file mode 100644
index 0000000000000000000000000000000000000000..674eba42ebc52579bd085bf15ee3044a4510a31b
--- /dev/null
+++ b/app/escriptorium/templates/core/models_list/main.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n staticfiles %}
+
+{% block body %}
+<a href="{% url 'model-upload' %}" class="btn btn-success float-sm-right">{% trans "Upload a model" %}</a>
+<h2>{% trans "My Models" %}</h2>
+{% include 'core/models_list/table.html' %}
+{% endblock %}
+{% block scripts %}
+    {% include 'core/models_list/scripts.html' %}
+{% endblock %}
diff --git a/app/escriptorium/templates/core/models_list/scripts.html b/app/escriptorium/templates/core/models_list/scripts.html
new file mode 100644
index 0000000000000000000000000000000000000000..766e258e5a59601843633c29d245ba2cbce7779d
--- /dev/null
+++ b/app/escriptorium/templates/core/models_list/scripts.html
@@ -0,0 +1,16 @@
+<script type="text/javascript">
+    'use strict';
+    {% if user.onboarding %}
+     const ONBOARDING_PAGE  = "onboarding_models";
+    {% endif %}
+</script>
+
+{{ block.super }}
+<script type="text/javascript">
+'use strict';
+$(document).ready(function() {
+  // join the ws room
+  joinDocumentRoom('{{document.pk}}');
+  bootModels();
+});
+</script>
diff --git a/app/escriptorium/templates/core/models_list.html b/app/escriptorium/templates/core/models_list/table.html
similarity index 86%
rename from app/escriptorium/templates/core/models_list.html
rename to app/escriptorium/templates/core/models_list/table.html
index 809d1a300b64f04368efc7ed750f99286bfd5ebe..d5731aa0680ab9de6dec94af8440683d291e943e 100644
--- a/app/escriptorium/templates/core/models_list.html
+++ b/app/escriptorium/templates/core/models_list/table.html
@@ -1,7 +1,5 @@
-{% extends 'core/document_nav.html' %}
 {% load i18n staticfiles %}
 
-{% block tab_content %}
 <table id="models-table" class="table table-hover">
   <tr>
     <th class="w-50"></th>
@@ -11,7 +9,7 @@
     <th>{% trans "Errors" %}</th>
     <th>{# buttons #}</th>
   </tr>
-  {% for model in document.ocr_models.all %}
+  {% for model in page_obj %}
   <tr id="tr-{{model.pk}}" class="model-head" data-id="{{model.pk}}">
     <td title="{% trans "Model name" %}">{{ model.name }}</td>
     <td title="{% trans "Model role" %}">{{ model.get_job_display }}</td>
@@ -69,23 +67,4 @@
   {% endfor %}
   {% endfor %}
 </table>
-{% endblock %}
-
-{% block scripts %}
-<script type="text/javascript">
-    'use strict';
-    {% if user.onboarding %}
-     const ONBOARDING_PAGE  = "onboarding_models";
-    {% endif %}
-</script>
-
-{{ block.super }}
-<script type="text/javascript">
-'use strict';
-$(document).ready(function() {
-  // join the ws room
-  joinDocumentRoom('{{document.pk}}');
-  bootModels();
-});
-</script>
-{% endblock %}
+{% include 'includes/pagination.html' %}
diff --git a/app/escriptorium/templates/core/ocrmodel_form.html b/app/escriptorium/templates/core/ocrmodel_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..57efaad14500f190df13ac6f7672e80818026f88
--- /dev/null
+++ b/app/escriptorium/templates/core/ocrmodel_form.html
@@ -0,0 +1,26 @@
+{% extends 'base.html' %}
+{% load i18n staticfiles bootstrap %}
+
+{% block body %}
+<h2>{% trans "Upload a model" %}</h2>
+<form method="post" class="inline-form" enctype="multipart/form-data" action="{% url 'model-upload' %}">
+    {% csrf_token %}
+    {% render_field form.file %}
+    {% render_field form.name %}
+    <button type="submit" value="" class="nav-item btn btn-success">{% trans 'Upload' %}</button>
+</form>
+{% endblock %}
+{% block scripts %}
+<script type="text/javascript">
+/* Handle a basic name field auto filling */
+const fileInput = document.getElementById('id_file');
+const nameInput = document.getElementById('id_name');
+let currentFileName = '';
+fileInput.onchange = function() {
+    if (fileInput.files.length && (!nameInput.value || nameInput.value == currentFileName)) {
+        nameInput.value = fileInput.files[0].name;
+        currentFileName = nameInput.value;
+    }
+};
+</script>
+{% endblock %}
diff --git a/app/escriptorium/templates/core/wizards/segment.html b/app/escriptorium/templates/core/wizards/segment.html
index 52adbc7aab7799afea7ff328cb47290c1fc1a592..311094d11466d18a7ea14f862594c32ddb3b0924 100644
--- a/app/escriptorium/templates/core/wizards/segment.html
+++ b/app/escriptorium/templates/core/wizards/segment.html
@@ -8,12 +8,7 @@
 {% block wizard_fields %}
 
 <div class="form-group">
-  <h5>{% trans "Upload a model" %}</h5>
-  {% render_field process_form.upload_model class="js-proc-settings" accept=".mlmodel" %}
-</div>
-
-<div class="form-group">
-  <h5>{% trans "Or select an existing one" %}</h5>
+  <h5>{% trans "Select a model" %}</h5>
   {% render_field process_form.seg_model class="js-proc-settings" %}
 </div>
 
diff --git a/app/escriptorium/templates/core/wizards/segtrain.html b/app/escriptorium/templates/core/wizards/segtrain.html
index 6b279d1480b149a0567e1c868826e263e17ec8b5..40cb84a694298efd8743a8fdb1e257e31f9f5f39 100644
--- a/app/escriptorium/templates/core/wizards/segtrain.html
+++ b/app/escriptorium/templates/core/wizards/segtrain.html
@@ -10,10 +10,6 @@
   <h5>{% trans "New model" %}</h5>
   {% render_field process_form.new_model class="js-proc-settings" %}
 </div>
-<div class="form-group">
-  <h5>{% trans "Or Upload a model" %}</h5>
-  {% render_field process_form.upload_model class="js-proc-settings" accept=".mlmodel,.clstm,.pronn" %}
-</div>
 <div class="form-group">
   <h5>{% trans "Or select an existing one" %}</h5>
   {% render_field process_form.segtrain_model class="js-proc-settings" %}
diff --git a/app/escriptorium/templates/core/wizards/train.html b/app/escriptorium/templates/core/wizards/train.html
index 458de59b479620fed26930257215e596f169c077..f96267bf4b2b5f354bfa576408dc626e616cb02c 100644
--- a/app/escriptorium/templates/core/wizards/train.html
+++ b/app/escriptorium/templates/core/wizards/train.html
@@ -14,12 +14,6 @@
   {% render_field process_form.new_model class="js-proc-settings" %}
 </div>
 
-<div class="form-group">
-  <h5>{% trans "Or upload a model" %}</h5>
-  {% render_field process_form.upload_model class="js-proc-settings" accept=".mlmodel,.clstm,.pronn" %}
-
-</div>
-
 <div class="form-group">
   <h5>{% trans "Or select an existing one" %}</h5>
   {% render_field process_form.train_model class="js-proc-settings" %}
diff --git a/app/escriptorium/templates/core/wizards/transcribe.html b/app/escriptorium/templates/core/wizards/transcribe.html
index 8226fd839cf7700df62bea7b640774b1b577e820..901b318639694e985e3576a3c250e50268513ddd 100644
--- a/app/escriptorium/templates/core/wizards/transcribe.html
+++ b/app/escriptorium/templates/core/wizards/transcribe.html
@@ -7,12 +7,7 @@
 
 {% block wizard_fields %}
 <div class="form-group">
-  <h5>{% trans "Upload model" %}</h5>
-  {% render_field process_form.upload_model class="js-proc-settings" data_document=document.pk accept=".mlmodel,.clstm,.pronn" %}
-</div>
-
-<div class="form-group">
-  <h5>{% trans "Or use existing model" %}</h5>
+  <h5>{% trans "Select a model" %}</h5>
   {% render_field process_form.ocr_model class="js-proc-settings" data_document=document.pk %}
 </div>
 {% endblock %}