How I Developed Hue's AI Art Tool with MindsDB & OpenAI's Dall-E Image Generation Model

How I Developed Hue's AI Art Tool with MindsDB & OpenAI's Dall-E Image Generation Model

Hue

A few months ago, Hue was a simple online art store where users could create an account to purchase and download artwork. Now, Hue is a sophisticated AI art & image generation tool where users can create, license, and even sell their awesome creations!

You probably think it took a lot of time and complexity to enhance my program with artificial intelligence. You may even think this is too complicated for you. But the truth is, I was able to add this functionality rather quickly and easily thanks to MindsDB's Python SDK... and so can you! Follow along to see how I did it.

Training Custom Models

To begin with, I headed over to https://cloud.mindsdb.com/ and trained custom models specific to each genre I selected.

Create Model

Street Art Model:

CREATE MODEL open_ai.street_art
PREDICT img_url
USING
   engine = 'openai',
   mode = 'image',
   prompt_template = '{{text}}, 4K | highly detailed, contemporary, urban street art style of Kaws combined with the controversial style of Banksy | bright lighting | warrm happy colors ';

I ran this query to confirm the model was done training.

Checking the Status of The Model

SELECT status
FROM open_ai.models
WHERE name='street_art';

Then, I ran a few test queries from MindsDB Cloud to see how accurate the model was based on the specified parameters.

Query The Model

SELECT *
FROM open_ai.street_art
WHERE text='A statue of a cool mouse with headphones, baggy pants, and a t-shirt on a side-street in NYC.'

Python/Django Integration

Next, I headed over to my Django/Python project and used this command to create a new app (named Hugo). (Because this is an AI, I gave it a clever name that is a play on 'Hue'):

python manage.py startapp hugo

Once that was ready, I created custom database models to support my new integration:

hugo/models.py

class Style(models.Model):
    name = models.CharField(max_length=254)
    friendly_name = models.CharField(max_length=254, null=True, blank=True)
    class Meta:
        verbose_name_plural = 'Styles'

    def __str__(self):
        return self.name

    def get_friendly_name(self):
        return self.friendly_name

class Artwork(models.Model):
    id = models.AutoField(primary_key=True)
    artwork_description = models.TextField(null=True, blank=True)
    title = models.CharField(max_length=254, unique=True)
    image_url = models.URLField(max_length=1024, null=True, blank=True)
    image = models.ImageField(null=True, blank=True, )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    deleted_at = models.DateTimeField(auto_now=True)
    # Use a foreign key to give each user ownership of their artwork
    user = models.ForeignKey(User, related_name='users', on_delete=models.CASCADE, blank=True, null=True)
    style = models.ForeignKey('Style', null=True, blank=True,on_delete=models.SET_NULL)
    # Boolean field so user can choose if artwork public
    is_public = models.BooleanField(default=False)
    # Boolean field so user can decide if artwork is downloadable
    is_downloadable = models.BooleanField(default=False)
    # Allows users to import artwork to store
    for_sale = models.BooleanField(default=False)
     # Allows users to set a price for artwork imported to store
    price = models.DecimalField(
    decimal_places=2, max_digits=8, null=False, default=0)
    in_import_queue = models.BooleanField(default=False)

    def get_style(l):
        return dict(Style.objects.filter).get(l)

    class Meta:
        verbose_name_plural = 'artworks'

    def save(self, *args, **kwargs):
             super().save(*args, **kwargs)

    def __str__(self):
        return self.title

And some views to render & sort the custom artwork on the front end:

hugo/views.py

def hugo(request):
    artworks = Artwork.objects.all()
    styles = None

    # sort artwork by style if the user has selected that style
    if request.GET:
        if 'style' in request.GET:
            styles = request.GET['style'].split(',')
            artworks = artworks.filter(style__name__in=styles)
            styles = Style.objects.filter(name__in=styles)
    # display artwork from newest to oldest whe visiting the page
    artworks = artworks.order_by('-created_at')
    context = {
        'artworks': artworks,
        'current_styles': styles,
        # 'sort_dir': sort_dir,
    }

    return render(request, 'hugo.html', context)

I then configured the urls for the hugo app and the entire project.

hugo/views/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.artwork, name='artwork'),
    path('<artwork_id>', views.artwork_detail, name='artwork_detail'),
    path('add_artwork/', views.add_artwork, name='add_artwork'),  
]

hue/urls.py

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from django.conf.urls import url, handler404
from home.views import page_not_found_view

handler404 = page_not_found_view

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('allauth.urls')),
    url(r'^accounts/', include('allauth.urls')),
    path('', include('home.urls')),
    path('page_not_found_view/', page_not_found_view, name='page_not_found_view'),
    path('shop/', include('shop.urls')),
    path('cart/', include('cart.urls')),
    path('checkout/', include('checkout.urls')),
    path('profile/', include('profiles.urls')),
    # New AI app is defined here
    path('hugo/', include('hugo.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
    static('django.views.static', (r'media/(?P<path>.*)', 'serve', {'document_root': settings.MEDIA_ROOT}),
)

Create Artwork

To get started, I created a form using fields from the Artwork model I created above:

forms.py

from django import forms
from .models import Artwork

class ArtworkForm(forms.ModelForm):
    class Meta:
        model = Artwork
        fields = ['title', 'style', 'artwork_description', 'is_downloadable', 'is_public']

    def __init__(self, *args, **kwargs):
            """
            Add placeholders and classes, remove auto-generated
            labels and set autofocus on first field
            """
            super().__init__(*args, **kwargs)

            for field_name, field in self.fields.items():
                """ Set a length requirement for the artwork description """
                if field_name == 'artwork_description':
                    field.widget.attrs['maxlength'] = 500
                    field.widget.attrs['rows'] = 3
                    field.widget.attrs['placeholder'] = 'Describe your artwork'
                    field.widget.attrs['class'] = 'border-1 rounded shadow-sm'
                    field.widget.attrs['required'] = True
                if field_name == 'name':
                    field.widget.attrs['placeholder'] = 'Give your artwork a name'
                    field.widget.attrs['class'] = 'border-1 rounded shadow-sm'
                    field.widget.attrs['required'] = True
                else:
                    field.widget.attrs['class'] = 'border-1 rounded shadow-sm'
                    field.widget.attrs['required'] = True

Then, I added a new view for rendering the Add Artwork form on the front end. As you will see below, the artwork generated by these models is returned as an image_url (a clickable link containing the image). Then, the 'add_hugo' method in hugo/views.py automates the process of downloading the image as a .jpg and assigns it to the artwork model.

hugo/views.py

@login_required
def add_hugo(request):
    if request.method == 'POST':
        form = ArtworkForm(request.POST, request.FILES)
        try:
            if form.is_valid():
                artwork = form.save(commit=False)
                artwork.user = request.user
                artwork.save()

                style = artwork.style 
                text = artwork.artwork_description
                encoded_text = quote(text)

                # Define a dictionary to map style names to query templates
                style_queries = {
                    'pop-art': 'SELECT * FROM open_ai.retro WHERE text="{}";',
                    'digital-art': 'SELECT * FROM open_ai.digital_only WHERE text="{}";',
                    'fine-art': 'SELECT * FROM open_ai.fine_art WHERE text="{}";',
                    'street-art': 'SELECT * FROM open_ai.urban_art WHERE text="{}";',
                    'abstract-art': 'SELECT * FROM open_ai.abstract WHERE text="{}";',
                    'photography': 'SELECT * FROM open_ai.photography WHERE text="{}";'
                }

                query_template = style_queries.get(style.name)

                if query_template is not None:
                    query = query_template.format(encoded_text)  # Insert encoded_text into the query template
                    mdb_server = mindsdb_sdk.connect('https://cloud.mindsdb.com', settings.MINDSDB_EMAIL, settings.MINDSDB_PASSWORD)
                    project = mdb_server.get_project('open_ai')
                    query_result = project.query(query).fetch()

                    ai_img = DataFrame.to_string(query_result)
                    # Extract the image URL from ai_img using regular expressions
                    url_pattern = r'(https?://\S+)'
                    match = re.search(url_pattern, ai_img)
                    if match:
                        image_url = match.group(1)
                    else:
                        # Handle the case where no valid URL is found
                        image_url = None
                else:
                    # Handle the case where the selected style is not recognized
                    image_url = None

                if image_url:
                    # Download the image from the URL
                    response = requests.get(image_url)
                    if response.status_code == 200:
                        img_temp = NamedTemporaryFile(delete=True)
                        img_temp.write(response.content)
                        img_temp.flush()

                        # Generate a random name for the image file
                        timestamp = str(int(time.time()))  # Get the current timestamp
                        random_str = ''.join(random.choices(string.ascii_lowercase, k=6))  # Generate a random string of length 6
                        image_filename = f'image_{timestamp}_{random_str}.jpg'

                        # Assign the downloaded image to the artwork model with the arandom name
                        artwork.image_url = image_url
                        artwork.image.save(image_filename, files.File(img_temp))
                        artwork.save()  # Save the artwork model again to update the image field

                return redirect(reverse('hugo'))
        except Exception as e:
            # Delete the artwork if an exception occurs
            artwork.delete()
            return render(request, 'runtime_error_template.html', {'error_message': str(e)})
    else:
        form = ArtworkForm()

        template = 'add_hugo.html'
        context = {'form': form}

        return render(request, template, context)

Next, I added the HTML form on the front end:

(Note: I used JavaScript to create a random prompt generator and add some defensive programming to prevent any errors from MindsDB and/or OpenAI)

<form method="POST" action="{% url 'add_hugo' %}" class="form mb-2" enctype="multipart/form-data"
    onsubmit="return showLoading() && validateForm()">
    {% csrf_token %}
    {% for field in form %}
    {% if field.name == 'artwork_description' %}
    {{ field | as_crispy_field }}
    <div class="text-start mt-3">
        <a type="button" class="body-font random-prompt" onclick="generateRandomPrompt()">Surprise
            Me!</a>
    </div>
    <div id="charCount" class="text-end mt-1">
        <small>Character count: <span id="charCountValue">0</span>/400</small>
    </div>
    {% else %}
    {{ field | as_crispy_field }}
    {% endif %}
    {% endfor %}
    <div class="text-right">
        <a class="btn btn-dark rounded-0" href="{% url 'hugo' %}">Cancel</a>
        </button>
        <button id="createArtBtn" class="btn btn-dark rounded-0" type="submit">Create Art</button>
    </div>
</form>
{% endblock %}

{% block postloadjs %}
{{ block.super }}
# random prompt generator
<script>
    function generateRandomPrompt() {
        var prompts = [
            "A surreal landscape with floating islands",
            "The Hulk, wearing an apron and baking a cake. Retro, 1950's aesthetic",
            "A futuristic cityscape at sunset",
            "...."
            ];

        var randomPrompt = prompts[Math.floor(Math.random() * prompts.length)];
        document.getElementById('id_artwork_description').value = randomPrompt;

        updateCharCount(randomPrompt); // Update character count after setting the prompt
    }

    function validateForm() {
        var descriptionField = document.getElementById('id_artwork_description');
        var descriptionValue = descriptionField.value.toLowerCase();

        var charCount = descriptionValue.length;
        if (charCount > 400) {
            alert('Description is too long. Please keep it under 400 characters.');
            return false; // Prevent form submission if character count exceeds 400
        }

        return true; // Proceed with form submission if validation passes
    }

    document.getElementById('id_artwork_description').addEventListener('input', function () {
        var charCount = this.value.length;
        document.getElementById('charCountValue').textContent = charCount;
    });

    function updateCharCount(text) {
        var charCount = text.length;
        document.getElementById('charCountValue').textContent = charCount;
    }
</script>

I even added functionality to handle runtime errors and prevent the creation of database rows with empty or invalid fields.

The images created by users are saved to the media folder, which was configured in the Django project's main settings.py file:

settings.py

# Media
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

MEDIAFILES_LOCATION = 'media'

# Override static and media URLs in production
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{STATICFILES_LOCATION}/'
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{MEDIAFILES_LOCATION}/'

Note: AWS_S3_CUSTOM_DOMAIN and other sensitive information are stored securely in the project's environment.

With all this in place, users can fill out the Add Artwork form on the front end it will send their artwork description to MindsDB as the {text} prompt in the query, SELECT * FROM open_ai.query_name WHERE text="{text}"; and return the user's AI image.

Resources

Hue

MindsDB:

MindsDB Python SDK

MindsDB's OpenAI Dall-E integration:

OpenAI

Dall-E