Skip to content

Add AI preference options #1911

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from django.utils.safestring import mark_safe
from multiupload_plus.fields import MultiFileField

from accounts.models import Profile, EmailPreferenceType, OldUsername, DeletedUser
from accounts.models import Profile, EmailPreferenceType, OldUsername, DeletedUser, AIPreference
from utils.encryption import sign_with_timestamp, unsign_with_timestamp
from utils.forms import HtmlCleaningCharField, HtmlCleaningCharFieldWithCenterTag, filename_has_valid_extension
from utils.spam import is_spam
Expand Down Expand Up @@ -352,13 +352,27 @@ class ProfileForm(forms.ModelForm):
allow_simultaneous_playback = forms.BooleanField(
label="Allow simultaneous audio playback", required=False, widget=forms.CheckboxInput(attrs={'class': 'bw-checkbox'}))
prefer_spectrograms = forms.BooleanField(
label="Show spectrograms in sound players by default", required=False, widget=forms.CheckboxInput(attrs={'class': 'bw-checkbox'}))
label="Show spectrograms in sound players by default", required=False, widget=forms.CheckboxInput(attrs={'class': 'bw-checkbox'}))

ai_sound_usage_preference = forms.ChoiceField(
label=mark_safe("<div class=\"v-spacing-1 text-grey\">I agree with my sounds being used to train generative AI models provided that:</div>"),
choices=AIPreference.AI_PREFERENCE_CHOICES,
required=False,
help_text=mark_safe("<div class=\"v-spacing-top-3 text-light-grey\">Use the setting above to express a "
"preference regarding the usage of your sounds for training generative Artificial Intelligence models. "
"This preference <b>applies to all your uploaded sounds</b>. Please, read the <a href=""><i>Usage of my sounds for "
"training genrative AI models</i> help section</a> to learn more about the details and implications of the available options.</div> ")
)

def __init__(self, request, *args, **kwargs):
self.request = request
kwargs.update(initial={
'username': request.user.username
})
initial_kwargs = {
'username': request.user.username,
}
ai_preference = request.user.profile.get_ai_preference()
if ai_preference:
initial_kwargs['ai_sound_usage_preference'] = ai_preference
kwargs.update(initial=initial_kwargs)
kwargs.update(dict(label_suffix=''))
super().__init__(*args, **kwargs)

Expand Down Expand Up @@ -396,6 +410,11 @@ def __init__(self, request, *args, **kwargs):
self.fields['sound_signature'].widget.attrs['class'] = 'unsecure-image-check'
self.fields['is_adult'].widget.attrs['class'] = 'bw-checkbox'
self.fields['not_shown_in_online_users_list'].widget = forms.HiddenInput()
self.fields['ui_theme_preference'].label = "User Interface theme preference:"

# If user has no sounds, do not show the AI sound usage preference field
if request.user.profile.num_sounds == 0:
self.fields.pop('ai_sound_usage_preference')
Comment on lines +416 to +417
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this mean that the process for a user to change their preference means that they first need to upload a sound and then go back to their profile? Can we make it clearer somehow?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far yes, but maybe we could think of redirecting users to the setting. Well, maybe not redirecting because I don't like that, but we could add a banner in the "manage sounds" page for users that have sounds and have no preference set explicitly. In this way, users that already have sounds will see the banner, and also right after describing a sound, when redirected to the manage sounds page, the banner will appear as well. Also I could move the AI preference to a new tab in the account settings page for more clarity and discoverability.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aaaaand we could email uploaders about that. It will be ~38k emails, so should be doable...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a good place to also verify that our email bounce code is working and use it to verify uploader accounts!


def clean_username(self):
username = self.cleaned_data["username"]
Expand Down
25 changes: 25 additions & 0 deletions accounts/migrations/0042_aipreference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.2.19 on 2025-05-23 15:39

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('accounts', '0041_alter_profile_options'),
]

operations = [
migrations.CreateModel(
name='AIPreference',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date_updated', models.DateTimeField(auto_now=True)),
('preference', models.CharField(choices=[('fr', "My sounds are used following Freesound's recommendations for interpreting Creative Commons licenses in a generative AI training context"), ('o', 'My sounds are used to train open models that are freely available to the public'), ('on', 'My sounds are used to train open models that are freely available to the public and that do not allow a commercial use')], default='fr', max_length=2)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='ai_preference', to=settings.AUTH_USER_MODEL)),
],
),
]
26 changes: 25 additions & 1 deletion accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,19 @@ def get_stats_for_profile_page(self):
cache.set(settings.USER_STATS_CACHE_KEY.format(self.user_id), stats_from_cache, 60*60*24)
stats_from_db.update(stats_from_cache)
return stats_from_db


def get_ai_preference(self):
try:
return self.user.ai_preference.preference
except AIPreference.DoesNotExist:
# If no preference is set, return the default one
return AIPreference.DEFAULT_AI_PREFERENCE

def set_ai_preference(self, preference_value):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this just create_or_update?
I don't think the .update is correct, you need to filter on the user first

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm you might be right, although I think it is strange that I did not test that. Anyway, I still need to add unit tests so I'll make sure this is covered and consider the reimplementation. Thanks!

num_updated = AIPreference.objects.update(user=self.user, preference=preference_value)
if num_updated == 0:
# If no AIPreference object was updated, it means no AIPreference object exister for that user. Create a new one.
AIPreference.objects.create(user=self.user, preference=preference_value)

class Meta:
ordering = ('-user__date_joined', )
Expand All @@ -657,6 +669,18 @@ class GdprAcceptance(models.Model):
date_accepted = models.DateTimeField(auto_now_add=True)


class AIPreference(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="ai_preference")
date_updated = models.DateTimeField(auto_now=True)
AI_PREFERENCE_CHOICES = (
("fr", "My sounds are used following Freesound's recommendations for interpreting Creative Commons licenses in a generative AI training context"),
Comment on lines +675 to +676
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a new way that we can do this is models.TextChoices: https://docs.djangoproject.com/en/5.2/ref/models/fields/#enumeration-types

Let's use descriptive names too, instead of these small 1-2 letter codes

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion, I'll look into that! The short cryptic names are because of DB char column length, but maybe the new field type already does some "translation", or maybe my assumption that limiting the chars would be more optimal is just not valid as PG will use some enum internally instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that TextChoices isn't a new field type, it's just a data type that makes it a bit easier to handle choices for a CharField, much more similar to a python enum.

Additionally, I was suggesting to not have a short column. In postgres there's no difference between a length 2, length 100, or unlimited length text field. Especially in our case where we only ever read the value. Let's make things easier on ourselves and make it readable when it's in the database.

There is a library which allows us to use a postgres enum in this column, which we could use if you want. I'm not too worried about it: https://github.com/django-commons/django-enum

("o", "My sounds are used to train open models that are freely available to the public"),
("on", "My sounds are used to train open models that are freely available to the public and that do not allow a commercial use")
)
DEFAULT_AI_PREFERENCE = "fr"
preference = models.CharField(max_length=2, choices=AI_PREFERENCE_CHOICES, default=DEFAULT_AI_PREFERENCE)


class UserFlag(models.Model):
user = models.ForeignKey(User, related_name="flags", on_delete=models.CASCADE)
reporting_user = models.ForeignKey(User, null=True, blank=True, default=None, on_delete=models.CASCADE)
Expand Down
5 changes: 5 additions & 0 deletions accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,11 +418,16 @@ def is_selected(prefix):
profile_form = ProfileForm(request, request.POST, instance=profile, prefix="profile")
old_sound_signature = profile.sound_signature
if profile_form.is_valid():
# Update AI sound usage preference, only if field present in the form
if 'ai_sound_usage_preference' in profile_form.cleaned_data:
profile.set_ai_preference(profile_form.cleaned_data['ai_sound_usage_preference'])

# Update username, this will create an entry in OldUsername
request.user.username = profile_form.cleaned_data['username']
request.user.save()
invalidate_user_template_caches(request.user.id)
profile.save()

msg_txt = "Your profile has been updated correctly."
if old_sound_signature != profile.sound_signature:
msg_txt += " Please note that it might take some time until your sound signature is updated in all your sounds."
Expand Down
12 changes: 11 additions & 1 deletion apiv2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
DEFAULT_FIELDS_IN_SOUND_DETAIL = 'id,url,name,tags,description,category,category_code,category_is_user_provided,geotag,created,license,type,channels,filesize,bitrate,' + \
'bitdepth,duration,samplerate,username,pack,pack_name,download,bookmark,previews,images,' + \
'num_downloads,avg_rating,num_ratings,rate,comments,num_comments,comment,similar_sounds,' + \
'analysis,analysis_frames,analysis_stats,is_explicit' # All except for analyzers
'analysis,analysis_frames,analysis_stats,is_explicit,author_ai_preference' # All except for analyzers
DEFAULT_FIELDS_IN_PACK_DETAIL = None # Separated by commas (None = all)


Expand Down Expand Up @@ -95,6 +95,7 @@ class Meta:
'geotag',
'created',
'license',
'author_ai_preference',
'type',
'channels',
'filesize',
Expand Down Expand Up @@ -165,6 +166,10 @@ def get_tags(self, obj):
def get_license(self, obj):
return obj.license.deed_url

author_ai_preference = serializers.SerializerMethodField()
def get_author_ai_preference(self, obj):
return obj.user.profile.get_ai_preference()

category = serializers.SerializerMethodField()
def get_category(self, obj):
category, subcategory = obj.category_names
Expand Down Expand Up @@ -404,6 +409,7 @@ class Meta:
'packs',
'num_posts',
'num_comments',
'ai_preference'
)

url = serializers.SerializerMethodField()
Expand Down Expand Up @@ -463,6 +469,10 @@ def get_num_posts(self, obj):
num_comments = serializers.SerializerMethodField()
def get_num_comments(self, obj):
return obj.comment_set.all().count()

ai_preference = serializers.SerializerMethodField()
def get_ai_preference(self, obj):
return obj.profile.get_ai_preference()


##################
Expand Down
8 changes: 8 additions & 0 deletions freesound/static/bw-frontend/src/components/tagsFormField.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ const prepareTagsFormFields = (container) => {
updateTags(inputElement, newTagsStr);
}, 200);
});

// Add click event listeners to the "add tag buttons" (if any). When clicked, these buttons will automatically add the tag to tge list
tagsFieldElement.getElementsByClassName('add-tag-button').forEach(button => {
button.addEventListener('click', evt => {
const tag = button.innerText
updateTags(inputElement, tag);
});
});
});
}

Expand Down
2 changes: 1 addition & 1 deletion sounds/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ def bulk_query(self, include_analyzers_output=False):
)

qs = self.select_related(
'user', 'user__profile', 'license', 'ticket', 'pack', 'geotag'
'user', 'user__profile', 'user__ai_preference', 'license', 'ticket', 'pack', 'geotag'
).annotate(
username=F("user__username"),
pack_name=F("pack__name"),
Expand Down
4 changes: 4 additions & 0 deletions templates/molecules/tags_form_field.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@
</div>
</div>
<span class="helptext">{{ form.tags.help_text|safe }}</span>
<div class="text-light-grey v-spacing-top-negative-2 v-spacing-3">If your sound was <b>created using Generative AI</b> models, please make sure to add the tag
<span class="add-tag-button cursor-pointer bg-white text-black border-grey-light text-center padding-1 no-text-wrap no-letter-spacing">GenAI</span>
and indicate the model in the description below.
</div>
</div>
Loading