From c7be748bfd21a9629d1ff021ffc6b0fa818d6a94 Mon Sep 17 00:00:00 2001 From: ffont Date: Mon, 6 Jul 2026 18:59:00 +0200 Subject: [PATCH 01/10] WIP re-working the ai preferences panel --- accounts/forms.py | 30 +++++----- accounts/models.py | 39 +++++++++---- accounts/urls.py | 1 + accounts/views.py | 43 ++++++++++---- templates/accounts/account_settings_base.html | 3 + templates/accounts/edit_ai_preferences.html | 58 +++++++++++++++++++ templates/accounts/manage_sounds.html | 7 --- 7 files changed, 136 insertions(+), 45 deletions(-) create mode 100644 templates/accounts/edit_ai_preferences.html diff --git a/accounts/forms.py b/accounts/forms.py index 473a7b7733..c70b5d8511 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -361,29 +361,12 @@ class ProfileForm(forms.ModelForm): required=False, widget=forms.CheckboxInput(attrs={"class": "bw-checkbox"}), ) - ai_sound_usage_preference = forms.ChoiceField( - label=mark_safe( - '
I agree with my sounds being used to train generative AI models provided that:
' - ), - choices=AIPreference.AI_PREFERENCE_CHOICES, - required=False, - help_text=mark_safe( - '
Use the setting above to express a ' - "preference regarding the usage of your sounds for training generative Artificial Intelligence models. " - 'This preference applies to all your uploaded sounds. Please, read the ' - "Usage of my sounds for " - "training generative AI models help section to learn more about the details and implications of the available options.
" - ), - ) def __init__(self, request, *args, **kwargs): self.request = request 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) @@ -507,6 +490,19 @@ def get_img_check_fields(self): return [self["about"], self["signature"], self["sound_signature"]] +class AIPreferenceForm(forms.Form): + ai_sound_usage_preference = forms.ChoiceField( + label="", + choices=AIPreference.AI_PREFERENCE_CHOICES, + widget=forms.RadioSelect(), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["ai_sound_usage_preference"].widget.attrs["class"] = "bw-radio" + self.ai_options_and_texts = AIPreference.AI_PREFERENCE_CHOICES_AND_EXPLANATION + + class EmailResetForm(forms.Form): email = forms.EmailField(label="New email address", max_length=254) password = forms.CharField(label="Your password", widget=forms.PasswordInput) diff --git a/accounts/models.py b/accounts/models.py index 5971487e4e..e61a36403a 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -722,17 +722,34 @@ class GdprAcceptance(models.Model): 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 = ( - ( - "freesound-cc-recommendation", - "My sounds are used following Freesound's recommendations for interpreting Creative Commons licenses in a generative AI training context", - ), - ("open-models", "My sounds are used to train open source models that are freely available to the public"), - ( - "open-noncommercial-models", - "My sounds are used to train open source models that are freely available to the public and that do not allow a commercial use", - ), - ) + + AI_PREFERENCE_CHOICES_AND_EXPLANATION = [ + { + "code": "no-additional-restrictions", + "label": "No additional restrictions", + "explanation": """this is the default option, and it means that sounds can be used for training Gen AI models as long as the recommendations described in the 2024 blog post are followed. Here is a summary of the recommendations: +
CC0 sounds can be used without restriction. +
CC-BY sounds can be used as long as the training set of the AI model is disclosed. +
CC-BY-NC sounds can be used as long as the training set of the AI model is disclosed and the model is not trained in a commercial setting or used for commercial purposes.""", + }, + { + "code": "open-models", + "label": "Open Models", + "explanation": "if this option is selected, the uploader allows their sounds to be used for training Gen AI models as long as these models are made open source* and freely available to the public. Note that selecting this option does not supersede the non-commercial clause of CC-BY-NC sounds, therefore model developers still need to exclude CC-BY-NC sounds if training a model in a commercial setting or if the model is to be used for commercial purposes.", + }, + { + "code": "open-noncommercial-models", + "label": "Open Non-Commercial Models", + "explanation": "this option is similar to the previous one, but with an additional restriction of not allowing the use of sounds for models which are trained in a commercial setting or that are used for commercial purposes. Note that by using this option, sound uploaders can restrict the use of their sounds for training commercial Gen AI models even if the individual sound license does not have the non-commercial clause (i.e. for CC0 and CC-BY sounds).", + }, + { + "code": "no-gen-ai", + "label": "No generative AI", + "explanation": "My sounds are not used to train any AI models", + }, + ] + + AI_PREFERENCE_CHOICES = [(item["code"], item["label"]) for item in AI_PREFERENCE_CHOICES_AND_EXPLANATION] DEFAULT_AI_PREFERENCE = "freesound-cc-recommendation" preference = models.CharField(choices=AI_PREFERENCE_CHOICES, default=DEFAULT_AI_PREFERENCE) diff --git a/accounts/urls.py b/accounts/urls.py index b7eb8a63ea..7ee98076d6 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -86,6 +86,7 @@ path("", accounts.home, name="accounts-home"), path("edit/", accounts.edit, name="accounts-edit"), path("email-settings/", accounts.edit_email_settings, name="accounts-email-settings"), + path("ai-preferences/", accounts.edit_ai_preferences, name="accounts-ai-preferences"), path("delete/", accounts.delete, name="accounts-delete"), path("attribution/", accounts.attribution, name="accounts-attribution"), path("download-attribution/", accounts.download_attribution, name="accounts-download-attribution"), diff --git a/accounts/views.py b/accounts/views.py index 0da4eb205f..d3c64b88f5 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -67,6 +67,7 @@ import utils.sound_upload from accounts.forms import ( + AIPreferenceForm, AvatarForm, BulkDescribeForm, DeleteUserForm, @@ -461,6 +462,37 @@ def edit_email_settings(request): return render(request, "accounts/edit_email_settings.html", tvars) +@login_required +@transaction.atomic() +def edit_ai_preferences(request): + profile = request.user.profile + + form = AIPreferenceForm(request.POST or None) + # Save a couple of variables that we will need later to check if they have changed and we need + # to mark user sounds as index dirty or show some messages to the user + old_ai_preference = str(profile.get_ai_preference(default_if_not_set=False)) + + if form.is_valid(): + # Update AI sound usage preference, only if field present in the form + if "ai_sound_usage_preference" in form.cleaned_data: + profile.set_ai_preference(form.cleaned_data["ai_sound_usage_preference"]) + + if old_ai_preference != profile.get_ai_preference(default_if_not_set=False): + Sound.objects.filter(user=request.user).update(is_index_dirty=True) + invalidate_user_template_caches(request.user.id) + + msg_txt = "Your AI preference options have been updated correctly." + messages.add_message(request, messages.INFO, msg_txt) + return HttpResponseRedirect(reverse("accounts-ai-preferences")) + else: + form = AIPreferenceForm( + initial={"ai_sound_usage_preference": profile.get_ai_preference(default_if_not_set=False)} + ) + + tvars = {"form": form, "activePage": "ai_preferences"} + return render(request, "accounts/edit_ai_preferences.html", tvars) + + @login_required @transaction.atomic() def edit(request): @@ -481,14 +513,9 @@ def is_selected(prefix): profile_form = ProfileForm(request, request.POST, instance=profile, prefix="profile") # Save a couple of variables that we will need later to check if they have changed and we need # to mark user sounds as index dirty or show some messages to the user - old_ai_preference = str(profile.get_ai_preference(default_if_not_set=False)) old_username = request.user.username 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"] try: @@ -501,11 +528,8 @@ def is_selected(prefix): request.user.refresh_from_db(fields=["username"]) else: profile.save() - # profile.refresh_from_db() # Refresh profile to get updated ai preference when calling get_ai_preference - if old_username != request.user.username or old_ai_preference != profile.get_ai_preference( - default_if_not_set=False - ): + if old_username != request.user.username: Sound.objects.filter(user=request.user).update(is_index_dirty=True) invalidate_user_template_caches(request.user.id) @@ -659,7 +683,6 @@ def process_filter_and_sort_options(request, sort_options, tab): "sounds_processing_count": sounds_processing_count, "sounds_pending_description_count": sounds_pending_description_count, "packs_count": packs_count, - "user_has_ai_preference_unset": request.user.profile.get_ai_preference(default_if_not_set=False) is None, } # Then do dedicated processing for each tab diff --git a/templates/accounts/account_settings_base.html b/templates/accounts/account_settings_base.html index 26b21293d1..4398fcb8c6 100644 --- a/templates/accounts/account_settings_base.html +++ b/templates/accounts/account_settings_base.html @@ -21,6 +21,9 @@

Account settings

+ diff --git a/templates/accounts/edit_ai_preferences.html b/templates/accounts/edit_ai_preferences.html new file mode 100644 index 0000000000..3d1203d646 --- /dev/null +++ b/templates/accounts/edit_ai_preferences.html @@ -0,0 +1,58 @@ +{% extends "accounts/account_settings_base.html" %} + +{% load static %} +{% load filter_img %} +{% load bw_templatetags %} +{% load util %} + +{% block title %}Gen AI preferences{% endblock %} + +{% block main_content %} + +

Generative AI preferences

+ +
+
{% csrf_token %} +

+ This section allows you to set your preferences regarding the use of your sounds for the purpose of training generative Artificial Intelligence (Gen AI) models. +

+

+ Beyond the usage terms defined by the specific Creative Commons license that is choosen for each of your sounds, you can define + additional usage preferences for the specific case of training Gen AI models. + These additional preferences are applied to all your uploaded sounds, and are independent of the individual sound licenses. Please, select one of the options below: +

+
+ {% if form.ai_sound_usage_preference.errors %} +
{{ form.ai_sound_usage_preference.errors }}
+ {% endif %} +
+ {% for ai_option in form.ai_options_and_texts %} +
+ +
+ {% endfor %} +
+
+

+ When referring to open source models, we refer to the definition of Open Source AI by the Open Source Initiative (OSI). In short, that means that for a model to be considered open source, it needs to be released under terms that grant the freedom to use, study, modify and share the model, including their code, model weights and training documentation. +

+

+ For more information about this topic, including how your preferences are applied, please read the + Usage of my sounds for training generative AI models help section. +

+ +
+ +
+ +{% endblock %} diff --git a/templates/accounts/manage_sounds.html b/templates/accounts/manage_sounds.html index 13483be314..eef5f231a0 100644 --- a/templates/accounts/manage_sounds.html +++ b/templates/accounts/manage_sounds.html @@ -129,13 +129,6 @@
Describing sounds is disabled... 😟
- {% if user_has_ai_preference_unset %} -
- {% bw_icon 'notification' %} You haven't set your preference regarding the usage of your sounds for training generative AI models. - While no preference is set, the default option will be used. - To disable this warning, please set your preference in your account settings page and save profile settings. -
- {% endif %} {% if tab == 'published' or tab == 'pending_moderation' %}
{% csrf_token %} From 0b7987b9ed0d763c1e42d9cfc5e8e9f27fefbc53 Mon Sep 17 00:00:00 2001 From: ffont Date: Tue, 7 Jul 2026 10:53:01 +0200 Subject: [PATCH 02/10] Add animation to toasts to make them more visible --- .../bw-frontend/src/components/toast.js | 31 +++++++++++-- .../bw-frontend/styles/molecules/toast.scss | 44 +++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/freesound/static/bw-frontend/src/components/toast.js b/freesound/static/bw-frontend/src/components/toast.js index 81002fd243..dc82725b06 100644 --- a/freesound/static/bw-frontend/src/components/toast.js +++ b/freesound/static/bw-frontend/src/components/toast.js @@ -1,4 +1,27 @@ let hideToastTimeout; +let hideToastAnimationTimeout; +const TOAST_ANIMATION_DURATION_MS = 260; + +const showToastElement = toastElement => { + clearTimeout(hideToastAnimationTimeout); + toastElement.classList.remove('toast--hiding'); + toastElement.style.display = 'block'; + + // Force reflow so the show animation restarts when toast is shown again. + void toastElement.offsetWidth; + toastElement.classList.add('toast--visible'); +}; + +const hideToastElement = toastElement => { + clearTimeout(hideToastAnimationTimeout); + toastElement.classList.remove('toast--visible'); + toastElement.classList.add('toast--hiding'); + + hideToastAnimationTimeout = setTimeout(() => { + toastElement.classList.remove('toast--hiding'); + toastElement.style.display = 'none'; + }, TOAST_ANIMATION_DURATION_MS); +}; const wrapTextInUl = text => { // We wrap toast messages in ul/li so that they are formatted the same when returned from Django and when directly triggered in javascript @@ -11,21 +34,21 @@ const wrapTextInUl = text => { export const showToast = (text, ulWrap) => { clearTimeout(hideToastTimeout); const toastElement = document.querySelector('[role="alert"]'); - toastElement.style.display = 'block'; + showToastElement(toastElement); if (ulWrap === true || ulWrap === undefined) { toastElement.children[0].innerHTML = wrapTextInUl(text); } else { toastElement.children[0].innerHTML = text; } hideToastTimeout = setTimeout(() => { - toastElement.style.display = 'none'; + hideToastElement(toastElement); }, 10000); }; export const showToastNoTimeout = (text, ulWrap) => { clearTimeout(hideToastTimeout); const toastElement = document.querySelector('[role="alert"]'); - toastElement.style.display = 'block'; + showToastElement(toastElement); if (ulWrap === true || ulWrap === undefined) { toastElement.children[0].innerHTML = wrapTextInUl(text); } else { @@ -36,5 +59,5 @@ export const showToastNoTimeout = (text, ulWrap) => { export const dismissToast = () => { clearTimeout(hideToastTimeout); const toastElement = document.querySelector('[role="alert"]'); - toastElement.style.display = 'none'; + hideToastElement(toastElement); }; diff --git a/freesound/static/bw-frontend/styles/molecules/toast.scss b/freesound/static/bw-frontend/styles/molecules/toast.scss index 572f8baf1b..902d28ca35 100644 --- a/freesound/static/bw-frontend/styles/molecules/toast.scss +++ b/freesound/static/bw-frontend/styles/molecules/toast.scss @@ -3,6 +3,8 @@ z-index: 99999; display: none; position: fixed; + opacity: 0; + transform: translateY(20px); padding: 14px 14px; width: 100%; @include md-and-above { @@ -30,3 +32,45 @@ padding: 0; } } + +.toast.toast--visible { + animation: toast-slide-up 260ms ease-out forwards; +} + +.toast.toast--hiding { + animation: toast-slide-down 260ms ease-in forwards; +} + +@keyframes toast-slide-up { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes toast-slide-down { + from { + opacity: 1; + transform: translateY(0); + } + + to { + opacity: 0; + transform: translateY(20px); + } +} + +@media (prefers-reduced-motion: reduce) { + .toast, + .toast.toast--visible, + .toast.toast--hiding { + animation: none; + opacity: 1; + transform: none; + } +} From eefb2f0f89f9b796e72e580bbfeb706ece7e1a3b Mon Sep 17 00:00:00 2001 From: ffont Date: Tue, 7 Jul 2026 11:24:52 +0200 Subject: [PATCH 03/10] Update AI preference text --- accounts/models.py | 22 +++++++------ accounts/views.py | 34 ++++++++++----------- templates/accounts/edit_ai_preferences.html | 14 ++++----- 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/accounts/models.py b/accounts/models.py index e61a36403a..446062fd8d 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -727,30 +727,32 @@ class AIPreference(models.Model): { "code": "no-additional-restrictions", "label": "No additional restrictions", - "explanation": """this is the default option, and it means that sounds can be used for training Gen AI models as long as the recommendations described in the 2024 blog post are followed. Here is a summary of the recommendations: -
CC0 sounds can be used without restriction. -
CC-BY sounds can be used as long as the training set of the AI model is disclosed. -
CC-BY-NC sounds can be used as long as the training set of the AI model is disclosed and the model is not trained in a commercial setting or used for commercial purposes.""", + "explanation": """Sounds can be used for training Gen AI models without any additional restrictions to those already set by the corresponding Creative Commons license. Here is a summary of the Creative Commons terms applied to the use case of training AI models: +
  • CC0 sounds can be used without restriction.
  • +
  • CC-BY sounds can be used as long as the training set of the AI model is disclosed.
  • +
  • CC-BY-NC sounds can be used as long as the training set of the AI model is disclosed and the model is not trained in a commercial setting or used for commercial purposes.
""", }, { "code": "open-models", - "label": "Open Models", - "explanation": "if this option is selected, the uploader allows their sounds to be used for training Gen AI models as long as these models are made open source* and freely available to the public. Note that selecting this option does not supersede the non-commercial clause of CC-BY-NC sounds, therefore model developers still need to exclude CC-BY-NC sounds if training a model in a commercial setting or if the model is to be used for commercial purposes.", + "label": "Open Models only", + "explanation": "Sounds can be used for training Gen AI models as long as the Creative Commons license terms summarised above are met, and as long as the trained models are made open source and freely available to the public. " + "Note that selecting this option does not supersede the non-commercial clause of CC-BY-NC sounds, therefore model developers still need to exclude CC-BY-NC sounds when training a model in a commercial setting or if the model is to be used for commercial purposes.", }, { "code": "open-noncommercial-models", - "label": "Open Non-Commercial Models", - "explanation": "this option is similar to the previous one, but with an additional restriction of not allowing the use of sounds for models which are trained in a commercial setting or that are used for commercial purposes. Note that by using this option, sound uploaders can restrict the use of their sounds for training commercial Gen AI models even if the individual sound license does not have the non-commercial clause (i.e. for CC0 and CC-BY sounds).", + "label": "Open Non-Commercial Models only", + "explanation": "Sounds can be used for training Gen AI models as long as the Creative Commons license terms summarised above are met, and as long as the trained models are made open source, freely available to the public, and not trained in a commercial setting nor used for commercial purposes. " + "Note that by using this option, sound uploaders can restrict the use of their sounds for training commercial Gen AI models even if the individual sound licenses do not have the non-commercial clause (i.e. for CC0 and CC-BY sounds).", }, { "code": "no-gen-ai", "label": "No generative AI", - "explanation": "My sounds are not used to train any AI models", + "explanation": "By selecting this option, sound uploaders expresses their preference that their uploaded sounds should not be used for training any type of generative AI models.", }, ] AI_PREFERENCE_CHOICES = [(item["code"], item["label"]) for item in AI_PREFERENCE_CHOICES_AND_EXPLANATION] - DEFAULT_AI_PREFERENCE = "freesound-cc-recommendation" + DEFAULT_AI_PREFERENCE = "no-additional-restrictions" preference = models.CharField(choices=AI_PREFERENCE_CHOICES, default=DEFAULT_AI_PREFERENCE) diff --git a/accounts/views.py b/accounts/views.py index d3c64b88f5..cb7aceb1c9 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -467,27 +467,27 @@ def edit_email_settings(request): def edit_ai_preferences(request): profile = request.user.profile - form = AIPreferenceForm(request.POST or None) - # Save a couple of variables that we will need later to check if they have changed and we need - # to mark user sounds as index dirty or show some messages to the user - old_ai_preference = str(profile.get_ai_preference(default_if_not_set=False)) - - if form.is_valid(): - # Update AI sound usage preference, only if field present in the form - if "ai_sound_usage_preference" in form.cleaned_data: + if request.method == "POST": + form = AIPreferenceForm(request.POST) + + if form.is_valid(): + # We get the existing preference so later we can compare if it has changed. + # Note that "default preference" is the same as "no preference", but we still want to + # save a "default preference" object so that we know if users have explicitly set it through the + # preferences panel. For this reason, here we don't want to get the default if the preference is not + # set, so that we can compare with the form value and properly create the object if needed. + old_ai_preference = str(profile.get_ai_preference(default_if_not_set=False)) profile.set_ai_preference(form.cleaned_data["ai_sound_usage_preference"]) - if old_ai_preference != profile.get_ai_preference(default_if_not_set=False): - Sound.objects.filter(user=request.user).update(is_index_dirty=True) - invalidate_user_template_caches(request.user.id) + # If preference has changed, mark all sounds as index dirty + if old_ai_preference != form.cleaned_data["ai_sound_usage_preference"]: + Sound.objects.filter(user=request.user).update(is_index_dirty=True) - msg_txt = "Your AI preference options have been updated correctly." - messages.add_message(request, messages.INFO, msg_txt) - return HttpResponseRedirect(reverse("accounts-ai-preferences")) + msg_txt = "Your Gen AI preference options have been updated correctly." + messages.add_message(request, messages.INFO, msg_txt) + return HttpResponseRedirect(reverse("accounts-ai-preferences")) else: - form = AIPreferenceForm( - initial={"ai_sound_usage_preference": profile.get_ai_preference(default_if_not_set=False)} - ) + form = AIPreferenceForm(initial={"ai_sound_usage_preference": profile.get_ai_preference()}) tvars = {"form": form, "activePage": "ai_preferences"} return render(request, "accounts/edit_ai_preferences.html", tvars) diff --git a/templates/accounts/edit_ai_preferences.html b/templates/accounts/edit_ai_preferences.html index 3d1203d646..63524e593c 100644 --- a/templates/accounts/edit_ai_preferences.html +++ b/templates/accounts/edit_ai_preferences.html @@ -14,10 +14,10 @@

Generative AI preferences

{% csrf_token %}

- This section allows you to set your preferences regarding the use of your sounds for the purpose of training generative Artificial Intelligence (Gen AI) models. + This section allows you to set your preferences regarding the use of your uploaded sounds for the purpose of training generative Artificial Intelligence (Gen AI) models.

- Beyond the usage terms defined by the specific Creative Commons license that is choosen for each of your sounds, you can define + Beyond the usage terms already defined by the specific Creative Commons license associated with each of your sounds, you can define additional usage preferences for the specific case of training Gen AI models. These additional preferences are applied to all your uploaded sounds, and are independent of the individual sound licenses. Please, select one of the options below:

@@ -34,8 +34,8 @@

Generative AI preferences

-
{{ ai_option.label }}
-
{{ ai_option.explanation|safe }}
+
{{ ai_option.label }}
+
{{ ai_option.explanation|safe }}
@@ -44,13 +44,13 @@
{{ ai_option.label }}

- When referring to open source models, we refer to the definition of Open Source AI by the Open Source Initiative (OSI). In short, that means that for a model to be considered open source, it needs to be released under terms that grant the freedom to use, study, modify and share the model, including their code, model weights and training documentation. + Note that when using the term open source models, we refer to the definition of Open Source AI by the Open Source Initiative (OSI). In short, that means that for a model to be considered open source, it needs to be released under terms that grant the freedom to use, study, modify and share the model, including their code, model weights and training documentation.

- For more information about this topic, including how your preferences are applied, please read the + For more information about this topic, including how these Gen AI preferences are applied, please read the Usage of my sounds for training generative AI models help section.

- + From 3b1078b55cdc88342b78c9974cc8e6efcf4bdf48 Mon Sep 17 00:00:00 2001 From: ffont Date: Wed, 8 Jul 2026 10:42:29 +0200 Subject: [PATCH 04/10] Rename ai_preference to gen_ai_preference, update texts, update tests --- _docs/api/source/resources.rst | 2 +- accounts/models.py | 20 ++++++------- accounts/tests/test_profile.py | 32 +++++++-------------- accounts/views.py | 8 +++--- apiv2/serializers.py | 18 ++++++------ templates/accounts/edit_ai_preferences.html | 17 +++++------ utils/search/backends/solr555pysolr.py | 2 +- utils/search/schema/freesound.json | 2 +- 8 files changed, 43 insertions(+), 58 deletions(-) diff --git a/_docs/api/source/resources.rst b/_docs/api/source/resources.rst index 9b73936307..29c42722b8 100644 --- a/_docs/api/source/resources.rst +++ b/_docs/api/source/resources.rst @@ -299,7 +299,7 @@ geotag string yes* Latitude and longitude o is_geotagged boolean yes Whether the sound has geotag information. created string yes The date when the sound was uploaded (e.g. "2014-04-16T20:07:11.145"). license string yes The Creative Commons license under which the sound is available to you ("Attribution", "Attribution NonCommercial", "Creative Commons 0"). -ai_preference string yes The user preference regarding the use of the sound for training generative AI models ("freesound-cc-recommendation", "open-models", "open-noncommercial-models"). Please check `our help section about generative AI training preferences `_ for more information about the meaning of these values. +gen_ai_preference string yes The user preference regarding the use of the sound for training generative AI models ("no-additional-restrictions", "open-models", "open-noncommercial-models", "no-gen-ai"). Please check `our help section about generative AI training preferences `_ for more information about the meaning of these values. type string yes The original type of the sound (wav, aif, aiff, ogg, mp3, m4a, or flac). channels integer yes The number of sound channels (mostly 1 or 2). filesize integer yes The size of the file in bytes. diff --git a/accounts/models.py b/accounts/models.py index 446062fd8d..1b7709ef18 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -695,7 +695,7 @@ def get_stats_for_profile_page(self): stats_from_db.update(stats_from_cache) return stats_from_db - def get_ai_preference(self, default_if_not_set=True): + def get_gen_ai_preference(self, default_if_not_set=True): try: return self.user.ai_preference.preference except AIPreference.DoesNotExist: @@ -704,7 +704,7 @@ def get_ai_preference(self, default_if_not_set=True): return AIPreference.DEFAULT_AI_PREFERENCE return None - def set_ai_preference(self, preference_value): + def set_gen_ai_preference(self, preference_value): AIPreference.objects.update_or_create(user=self.user, defaults={"preference": preference_value}) class Meta: @@ -727,27 +727,25 @@ class AIPreference(models.Model): { "code": "no-additional-restrictions", "label": "No additional restrictions", - "explanation": """Sounds can be used for training Gen AI models without any additional restrictions to those already set by the corresponding Creative Commons license. Here is a summary of the Creative Commons terms applied to the use case of training AI models: -
  • CC0 sounds can be used without restriction.
  • -
  • CC-BY sounds can be used as long as the training set of the AI model is disclosed.
  • -
  • CC-BY-NC sounds can be used as long as the training set of the AI model is disclosed and the model is not trained in a commercial setting or used for commercial purposes.
""", + "explanation": """No additional statement, your sounds can be used for the purpose of training Gen AI models in accordance with the Creative Commons license terms. + Read here for more details.""", }, { "code": "open-models", "label": "Open Models only", - "explanation": "Sounds can be used for training Gen AI models as long as the Creative Commons license terms summarised above are met, and as long as the trained models are made open source and freely available to the public. " - "Note that selecting this option does not supersede the non-commercial clause of CC-BY-NC sounds, therefore model developers still need to exclude CC-BY-NC sounds when training a model in a commercial setting or if the model is to be used for commercial purposes.", + "explanation": """Your sounds can be used for the purpose of training Gen AI models in accordance with the Creative Commons license terms and as long as the trained models are made open source and freely available to the public. + Read here for more details.""", }, { "code": "open-noncommercial-models", "label": "Open Non-Commercial Models only", - "explanation": "Sounds can be used for training Gen AI models as long as the Creative Commons license terms summarised above are met, and as long as the trained models are made open source, freely available to the public, and not trained in a commercial setting nor used for commercial purposes. " - "Note that by using this option, sound uploaders can restrict the use of their sounds for training commercial Gen AI models even if the individual sound licenses do not have the non-commercial clause (i.e. for CC0 and CC-BY sounds).", + "explanation": """Your sounds can be used for the purpose of training Gen AI models in accordance with the Creative Commons license terms and as long as the trained models are made open source, freely available to the public, and not trained in a commercial setting nor used for commercial purposes. + Read here for more details.""", }, { "code": "no-gen-ai", "label": "No generative AI", - "explanation": "By selecting this option, sound uploaders expresses their preference that their uploaded sounds should not be used for training any type of generative AI models.", + "explanation": """Your sounds can not be used for the purpose of training Gen AI models. Read here for more details.""", }, ] diff --git a/accounts/tests/test_profile.py b/accounts/tests/test_profile.py index 98433e0fc7..2d4dd8a212 100644 --- a/accounts/tests/test_profile.py +++ b/accounts/tests/test_profile.py @@ -216,46 +216,36 @@ def test_edit_user_profile_ai_preference(self): user.profile.save() # Check that user does not start with any preference set - self.assertEqual(user.profile.get_ai_preference(default_if_not_set=False), None) + self.assertEqual(user.profile.get_gen_ai_preference(default_if_not_set=False), None) - # Check that "get_ai_preference" returns the default one when no preference is set and default_if_not_set is True (default) - self.assertEqual(user.profile.get_ai_preference(), AIPreference.DEFAULT_AI_PREFERENCE) + # Check that "get_gen_ai_preference" returns the default one when no preference is set and default_if_not_set is True (default) + self.assertEqual(user.profile.get_gen_ai_preference(), AIPreference.DEFAULT_AI_PREFERENCE) # Now user edits preference in profile page self.client.force_login(user) new_preference = "open-models" - self.client.post( - "/home/edit/", + resp = self.client.post( + "/home/ai-preferences/", { - "profile-home_page": "http://www.example.com/", - "profile-username": "testuser", - "profile-about": "About test text", - "profile-signature": "Signature test text", - "profile-ui_theme_preference": "d", - "profile-ai_sound_usage_preference": new_preference, + "ai_sound_usage_preference": new_preference, }, ) # Check that new preference is set and that sounds were marked as dirty user = User.objects.select_related("profile").get(username="testuser") - self.assertEqual(user.profile.get_ai_preference(), new_preference) + self.assertEqual(user.profile.get_gen_ai_preference(), new_preference) self.assertEqual(Sound.objects.filter(user=user, is_index_dirty=True).count(), len(sounds)) # Now that there's an AI preference object already existing, try to change preference again and check that it works as expected - even_newer_preference = "freesound-cc-recommendation" + even_newer_preference = "no-additional-restrictions" self.client.post( - "/home/edit/", + "/home/ai-preferences/", { - "profile-home_page": "http://www.example.com/", - "profile-username": "testuser", - "profile-about": "About test text", - "profile-signature": "Signature test text", - "profile-ui_theme_preference": "d", - "profile-ai_sound_usage_preference": even_newer_preference, + "ai_sound_usage_preference": even_newer_preference, }, ) user = User.objects.select_related("profile").get(username="testuser") - self.assertEqual(user.profile.get_ai_preference(), even_newer_preference) + self.assertEqual(user.profile.get_gen_ai_preference(), even_newer_preference) def test_edit_user_email_settings(self): EmailPreferenceType.objects.create(name="email", display_name="email") diff --git a/accounts/views.py b/accounts/views.py index cb7aceb1c9..c5a8d4ae70 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -476,8 +476,8 @@ def edit_ai_preferences(request): # save a "default preference" object so that we know if users have explicitly set it through the # preferences panel. For this reason, here we don't want to get the default if the preference is not # set, so that we can compare with the form value and properly create the object if needed. - old_ai_preference = str(profile.get_ai_preference(default_if_not_set=False)) - profile.set_ai_preference(form.cleaned_data["ai_sound_usage_preference"]) + old_ai_preference = str(profile.get_gen_ai_preference(default_if_not_set=False)) + profile.set_gen_ai_preference(form.cleaned_data["ai_sound_usage_preference"]) # If preference has changed, mark all sounds as index dirty if old_ai_preference != form.cleaned_data["ai_sound_usage_preference"]: @@ -487,7 +487,7 @@ def edit_ai_preferences(request): messages.add_message(request, messages.INFO, msg_txt) return HttpResponseRedirect(reverse("accounts-ai-preferences")) else: - form = AIPreferenceForm(initial={"ai_sound_usage_preference": profile.get_ai_preference()}) + form = AIPreferenceForm(initial={"ai_sound_usage_preference": profile.get_gen_ai_preference()}) tvars = {"form": form, "activePage": "ai_preferences"} return render(request, "accounts/edit_ai_preferences.html", tvars) @@ -528,7 +528,7 @@ def is_selected(prefix): request.user.refresh_from_db(fields=["username"]) else: profile.save() - # profile.refresh_from_db() # Refresh profile to get updated ai preference when calling get_ai_preference + # profile.refresh_from_db() # Refresh profile to get updated ai preference when calling get_gen_ai_preference if old_username != request.user.username: Sound.objects.filter(user=request.user).update(is_index_dirty=True) invalidate_user_template_caches(request.user.id) diff --git a/apiv2/serializers.py b/apiv2/serializers.py index c06786ccf3..f0227876df 100644 --- a/apiv2/serializers.py +++ b/apiv2/serializers.py @@ -42,7 +42,7 @@ + "geotag,is_geotagged,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," - + "is_explicit,is_remix,was_remixed,md5,ai_preference" + + "is_explicit,is_remix,was_remixed,md5,gen_ai_preference" ) DEFAULT_FIELDS_IN_PACK_DETAIL = None # Separated by commas (None = all) @@ -145,7 +145,7 @@ class Meta: "is_geotagged", "created", "license", - "ai_preference", + "gen_ai_preference", "type", "channels", "filesize", @@ -222,10 +222,10 @@ def get_tags(self, obj): def get_license(self, obj): return obj.license.deed_url - ai_preference = serializers.SerializerMethodField() + gen_ai_preference = serializers.SerializerMethodField() - def get_ai_preference(self, obj): - return obj.user.profile.get_ai_preference() + def get_gen_ai_preference(self, obj): + return obj.user.profile.get_gen_ai_preference() category = serializers.SerializerMethodField() @@ -445,7 +445,7 @@ class Meta: "packs", "num_posts", "num_comments", - "ai_preference", + "gen_ai_preference", ) url = serializers.SerializerMethodField() @@ -522,10 +522,10 @@ def get_num_posts(self, obj): def get_num_comments(self, obj): return obj.comment_set.all().count() - ai_preference = serializers.SerializerMethodField() + gen_ai_preference = serializers.SerializerMethodField() - def get_ai_preference(self, obj): - return obj.profile.get_ai_preference() + def get_gen_ai_preference(self, obj): + return obj.profile.get_gen_ai_preference() ################## diff --git a/templates/accounts/edit_ai_preferences.html b/templates/accounts/edit_ai_preferences.html index 63524e593c..f0eeadc75a 100644 --- a/templates/accounts/edit_ai_preferences.html +++ b/templates/accounts/edit_ai_preferences.html @@ -17,9 +17,9 @@

Generative AI preferences

This section allows you to set your preferences regarding the use of your uploaded sounds for the purpose of training generative Artificial Intelligence (Gen AI) models.

- Beyond the usage terms already defined by the specific Creative Commons license associated with each of your sounds, you can define - additional usage preferences for the specific case of training Gen AI models. - These additional preferences are applied to all your uploaded sounds, and are independent of the individual sound licenses. Please, select one of the options below: + Sounds may be used for Gen AI training in accordance with your chosen Creative Commons license terms. + You may choose to add a statement to express additional usage terms for the specific case of training Gen AI models, however there is no guarantee that these will have any legal effect. + Please, select one of the options below:

{% if form.ai_sound_usage_preference.errors %} @@ -43,13 +43,10 @@
{{ ai_option.label }}
{% endfor %}
-

- Note that when using the term open source models, we refer to the definition of Open Source AI by the Open Source Initiative (OSI). In short, that means that for a model to be considered open source, it needs to be released under terms that grant the freedom to use, study, modify and share the model, including their code, model weights and training documentation. -

-

- For more information about this topic, including how these Gen AI preferences are applied, please read the - Usage of my sounds for training generative AI models help section. -

+
+ For more details about Generative AI preferences, including the way in which such preferences are treated, please read the + Usage of my sounds for training generative AI models help section. +
diff --git a/utils/search/backends/solr555pysolr.py b/utils/search/backends/solr555pysolr.py index f2c263c918..26e805e207 100644 --- a/utils/search/backends/solr555pysolr.py +++ b/utils/search/backends/solr555pysolr.py @@ -331,7 +331,7 @@ def convert_sound_to_search_engine_document(self, sound): document["preview_path"] = locations["preview"]["LQ"]["mp3"]["path"] # Index uploader AI preference - document["ai_preference"] = sound.user.profile.get_ai_preference() + document["gen_ai_preference"] = sound.user.profile.get_gen_ai_preference() # Index consolidated audio descriptors descriptors_to_index = {} diff --git a/utils/search/schema/freesound.json b/utils/search/schema/freesound.json index c57f91978b..6d298d9e1c 100644 --- a/utils/search/schema/freesound.json +++ b/utils/search/schema/freesound.json @@ -246,7 +246,7 @@ "stored": false }, { - "name": "ai_preference", + "name": "gen_ai_preference", "type": "string", "indexed": true, "stored": false From e329e89a54eda3f958bcba477de12ec0d314ffa6 Mon Sep 17 00:00:00 2001 From: ffont Date: Wed, 8 Jul 2026 10:58:43 +0200 Subject: [PATCH 05/10] Remove gen_ai_preference from user serializer as it is not relevant there Note that sound might have preference overrides in some situations (now or in the future), so therefore it is better not to add the preference at the user level in the API --- apiv2/serializers.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apiv2/serializers.py b/apiv2/serializers.py index f0227876df..41cab24e8e 100644 --- a/apiv2/serializers.py +++ b/apiv2/serializers.py @@ -445,7 +445,6 @@ class Meta: "packs", "num_posts", "num_comments", - "gen_ai_preference", ) url = serializers.SerializerMethodField() @@ -522,11 +521,6 @@ def get_num_posts(self, obj): def get_num_comments(self, obj): return obj.comment_set.all().count() - gen_ai_preference = serializers.SerializerMethodField() - - def get_gen_ai_preference(self, obj): - return obj.profile.get_gen_ai_preference() - ################## # PACK SERIALIZERS From fa6cbbe499794c9723a4900f13685d02613f2257 Mon Sep 17 00:00:00 2001 From: ffont Date: Wed, 8 Jul 2026 12:34:36 +0200 Subject: [PATCH 06/10] Add AI preference for voice recordings, other fixes and improvements --- accounts/forms.py | 7 +++ ...43_aipreference_opt_out_speech_and_more.py | 23 ++++++++++ accounts/models.py | 44 ++++++++++++++----- accounts/tests/test_profile.py | 9 +++- accounts/views.py | 18 ++++++-- apiv2/serializers.py | 2 +- freesound/settings.py | 10 +++++ sounds/models.py | 3 ++ templates/accounts/edit_ai_preferences.html | 13 +++++- utils/search/backends/solr555pysolr.py | 6 +-- 10 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 accounts/migrations/0043_aipreference_opt_out_speech_and_more.py diff --git a/accounts/forms.py b/accounts/forms.py index c70b5d8511..5ce7503863 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -496,11 +496,18 @@ class AIPreferenceForm(forms.Form): choices=AIPreference.AI_PREFERENCE_CHOICES, widget=forms.RadioSelect(), ) + opt_out_speech = forms.BooleanField( + label=mark_safe( + "Do not allow sounds classified under the \"Speech > Solo speech\" BST category to be used for the purpose of training Gen AI models." + ), + required=False, + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["ai_sound_usage_preference"].widget.attrs["class"] = "bw-radio" self.ai_options_and_texts = AIPreference.AI_PREFERENCE_CHOICES_AND_EXPLANATION + self.fields["opt_out_speech"].widget.attrs["class"] = "bw-checkbox" class EmailResetForm(forms.Form): diff --git a/accounts/migrations/0043_aipreference_opt_out_speech_and_more.py b/accounts/migrations/0043_aipreference_opt_out_speech_and_more.py new file mode 100644 index 0000000000..43f3699629 --- /dev/null +++ b/accounts/migrations/0043_aipreference_opt_out_speech_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.27 on 2026-07-08 09:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0042_aipreference'), + ] + + operations = [ + migrations.AddField( + model_name='aipreference', + name='opt_out_speech', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='aipreference', + name='preference', + field=models.CharField(choices=[('no-additional-restrictions', 'No additional restrictions'), ('open-models', 'Open Models only'), ('open-noncommercial-models', 'Open Non-Commercial Models only'), ('no-gen-ai', 'No generative AI')], default='no-additional-restrictions'), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 1b7709ef18..36643c0a59 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -695,17 +695,39 @@ def get_stats_for_profile_page(self): stats_from_db.update(stats_from_cache) return stats_from_db - def get_gen_ai_preference(self, default_if_not_set=True): + def get_gen_ai_preference(self, default_if_not_set=True, category_code=None): + """ + Returns the user's generative AI preference. + If no preference is set, returns the default preference if default_if_not_set is True, otherwise None. + If "category_code" argument is specified, this method will consider if there should be any preference overrides + because of that. + """ try: - return self.user.ai_preference.preference + preference_object = self.user.ai_preference + if ( + category_code is not None + and preference_object.opt_out_speech is True + and category_code in settings.AI_OPT_OUT_SPEECH_TAXONOMY_CODES + ): + return settings.AI_PREF_NO_GEN_AI + return preference_object.preference except AIPreference.DoesNotExist: # If no preference is set, return the default one if default_if_not_set: - return AIPreference.DEFAULT_AI_PREFERENCE + return settings.DEFAULT_AI_PREFERENCE return None - def set_gen_ai_preference(self, preference_value): - AIPreference.objects.update_or_create(user=self.user, defaults={"preference": preference_value}) + def set_gen_ai_preference(self, preference_value, opt_out_speech): + preference, _ = AIPreference.objects.update_or_create(user=self.user, defaults={"preference": preference_value}) + if opt_out_speech != preference.opt_out_speech: + preference.opt_out_speech = opt_out_speech + preference.save() + + def get_gen_ai_preference_opt_out_speech(self): + try: + return self.user.ai_preference.opt_out_speech + except AIPreference.DoesNotExist: + return settings.DEFAULT_OPT_OUT_SPEECH class Meta: ordering = ("-user__date_joined",) @@ -725,33 +747,33 @@ class AIPreference(models.Model): AI_PREFERENCE_CHOICES_AND_EXPLANATION = [ { - "code": "no-additional-restrictions", + "code": settings.AI_PREF_NO_ADDITIONAL_RESTRICTIONS, "label": "No additional restrictions", "explanation": """No additional statement, your sounds can be used for the purpose of training Gen AI models in accordance with the Creative Commons license terms. Read here for more details.""", }, { - "code": "open-models", + "code": settings.AI_PREF_OPEN_MODELS, "label": "Open Models only", "explanation": """Your sounds can be used for the purpose of training Gen AI models in accordance with the Creative Commons license terms and as long as the trained models are made open source and freely available to the public. Read here for more details.""", }, { - "code": "open-noncommercial-models", + "code": settings.AI_PREF_OPEN_NONCOMMERCIAL_MODELS, "label": "Open Non-Commercial Models only", "explanation": """Your sounds can be used for the purpose of training Gen AI models in accordance with the Creative Commons license terms and as long as the trained models are made open source, freely available to the public, and not trained in a commercial setting nor used for commercial purposes. Read here for more details.""", }, { - "code": "no-gen-ai", + "code": settings.AI_PREF_NO_GEN_AI, "label": "No generative AI", "explanation": """Your sounds can not be used for the purpose of training Gen AI models. Read here for more details.""", }, ] AI_PREFERENCE_CHOICES = [(item["code"], item["label"]) for item in AI_PREFERENCE_CHOICES_AND_EXPLANATION] - DEFAULT_AI_PREFERENCE = "no-additional-restrictions" - preference = models.CharField(choices=AI_PREFERENCE_CHOICES, default=DEFAULT_AI_PREFERENCE) + preference = models.CharField(choices=AI_PREFERENCE_CHOICES, default=settings.DEFAULT_AI_PREFERENCE) + opt_out_speech = models.BooleanField(default=settings.DEFAULT_OPT_OUT_SPEECH) class UserFlag(models.Model): diff --git a/accounts/tests/test_profile.py b/accounts/tests/test_profile.py index 2d4dd8a212..ffa4793418 100644 --- a/accounts/tests/test_profile.py +++ b/accounts/tests/test_profile.py @@ -36,7 +36,7 @@ import accounts.models from accounts.management.commands.process_email_bounces import decode_idna_email, process_message -from accounts.models import AIPreference, EmailBounce, EmailPreferenceType, UserEmailSetting +from accounts.models import EmailBounce, EmailPreferenceType, UserEmailSetting from accounts.views import handle_uploaded_image from forum.models import Forum, Post, Thread from geotags.models import GeoTag @@ -217,9 +217,10 @@ def test_edit_user_profile_ai_preference(self): # Check that user does not start with any preference set self.assertEqual(user.profile.get_gen_ai_preference(default_if_not_set=False), None) + self.assertEqual(user.profile.get_gen_ai_preference_opt_out_speech(), settings.DEFAULT_OPT_OUT_SPEECH) # Check that "get_gen_ai_preference" returns the default one when no preference is set and default_if_not_set is True (default) - self.assertEqual(user.profile.get_gen_ai_preference(), AIPreference.DEFAULT_AI_PREFERENCE) + self.assertEqual(user.profile.get_gen_ai_preference(), settings.DEFAULT_AI_PREFERENCE) # Now user edits preference in profile page self.client.force_login(user) @@ -228,12 +229,14 @@ def test_edit_user_profile_ai_preference(self): "/home/ai-preferences/", { "ai_sound_usage_preference": new_preference, + "opt_out_speech": True, }, ) # Check that new preference is set and that sounds were marked as dirty user = User.objects.select_related("profile").get(username="testuser") self.assertEqual(user.profile.get_gen_ai_preference(), new_preference) + self.assertEqual(user.profile.get_gen_ai_preference_opt_out_speech(), True) self.assertEqual(Sound.objects.filter(user=user, is_index_dirty=True).count(), len(sounds)) # Now that there's an AI preference object already existing, try to change preference again and check that it works as expected @@ -242,10 +245,12 @@ def test_edit_user_profile_ai_preference(self): "/home/ai-preferences/", { "ai_sound_usage_preference": even_newer_preference, + "opt_out_speech": False, }, ) user = User.objects.select_related("profile").get(username="testuser") self.assertEqual(user.profile.get_gen_ai_preference(), even_newer_preference) + self.assertEqual(user.profile.get_gen_ai_preference_opt_out_speech(), False) def test_edit_user_email_settings(self): EmailPreferenceType.objects.create(name="email", display_name="email") diff --git a/accounts/views.py b/accounts/views.py index c5a8d4ae70..be659f009e 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -477,17 +477,29 @@ def edit_ai_preferences(request): # preferences panel. For this reason, here we don't want to get the default if the preference is not # set, so that we can compare with the form value and properly create the object if needed. old_ai_preference = str(profile.get_gen_ai_preference(default_if_not_set=False)) - profile.set_gen_ai_preference(form.cleaned_data["ai_sound_usage_preference"]) + old_ai_opt_out_speech = profile.get_gen_ai_preference_opt_out_speech() + + profile.set_gen_ai_preference( + form.cleaned_data["ai_sound_usage_preference"], form.cleaned_data["opt_out_speech"] + ) # If preference has changed, mark all sounds as index dirty - if old_ai_preference != form.cleaned_data["ai_sound_usage_preference"]: + if ( + old_ai_preference != form.cleaned_data["ai_sound_usage_preference"] + or old_ai_opt_out_speech != form.cleaned_data["opt_out_speech"] + ): Sound.objects.filter(user=request.user).update(is_index_dirty=True) msg_txt = "Your Gen AI preference options have been updated correctly." messages.add_message(request, messages.INFO, msg_txt) return HttpResponseRedirect(reverse("accounts-ai-preferences")) else: - form = AIPreferenceForm(initial={"ai_sound_usage_preference": profile.get_gen_ai_preference()}) + form = AIPreferenceForm( + initial={ + "ai_sound_usage_preference": profile.get_gen_ai_preference(), + "opt_out_speech": profile.get_gen_ai_preference_opt_out_speech(), + } + ) tvars = {"form": form, "activePage": "ai_preferences"} return render(request, "accounts/edit_ai_preferences.html", tvars) diff --git a/apiv2/serializers.py b/apiv2/serializers.py index 41cab24e8e..50f655c003 100644 --- a/apiv2/serializers.py +++ b/apiv2/serializers.py @@ -225,7 +225,7 @@ def get_license(self, obj): gen_ai_preference = serializers.SerializerMethodField() def get_gen_ai_preference(self, obj): - return obj.user.profile.get_gen_ai_preference() + return obj.user.profile.get_gen_ai_preference(category_code=obj.category_code) category = serializers.SerializerMethodField() diff --git a/freesound/settings.py b/freesound/settings.py index fb688ac9bb..396d7fcd55 100644 --- a/freesound/settings.py +++ b/freesound/settings.py @@ -663,6 +663,16 @@ def load_broad_sound_taxonomy_from_csv(path): SEARCH_LOG_SLOW_QUERIES_MS_THRESHOLD = 1000 # Log search queries that take longer than this threshold in milliseconds. Set it to -1 to disable logging of slow queries. SEARCH_LOG_SLOW_QUERIES_QUERY_BASE_URL = "http://localhost:8983/solr/freesound/select/" +# ------------------------------------------------------------------------------- +# AI preferences panel +AI_PREF_NO_ADDITIONAL_RESTRICTIONS = "no-additional-restrictions" +AI_PREF_OPEN_MODELS = "open-models" +AI_PREF_OPEN_NONCOMMERCIAL_MODELS = "open-noncommercial-models" +AI_PREF_NO_GEN_AI = "no-gen-ai" +DEFAULT_AI_PREFERENCE = AI_PREF_NO_ADDITIONAL_RESTRICTIONS +DEFAULT_OPT_OUT_SPEECH = False +AI_OPT_OUT_SPEECH_TAXONOMY_CODES = ["sp-s"] # Solo-speech + # ------------------------------------------------------------------------------- # Tag recommendation client settings TAGRECOMMENDATION_ADDRESS = "tagrecommendation" diff --git a/sounds/models.py b/sounds/models.py index b981ad84b4..2d336c5d8c 100644 --- a/sounds/models.py +++ b/sounds/models.py @@ -1770,6 +1770,9 @@ def get_second_level_category_search_url(self): else: return None + def get_gen_ai_preference(self): + return self.user.profile.get_gen_ai_preference(category_code=self.category_code) + def estimate_bpm_from_metadata(self, min_bpm=25, max_bpm=300): """ Estimate the bpm of a sound by looking at its description, tags and name. diff --git a/templates/accounts/edit_ai_preferences.html b/templates/accounts/edit_ai_preferences.html index f0eeadc75a..b56e7b44c3 100644 --- a/templates/accounts/edit_ai_preferences.html +++ b/templates/accounts/edit_ai_preferences.html @@ -42,8 +42,19 @@
{{ ai_option.label }}
{% endfor %} + +
+ Complementarily to the options above, you can make an extra statement to opt-out from Gen AI model training for the particular case of voice recordings. + This option is designed to avoid the use of such sounds for training models that may generate speech that could be misused to impersonate real people. + Mark the checkbox below to enable this option: +
+
+ {{ form.opt_out_speech.errors }} + + {{ form.opt_out_speech }} +
-
+
For more details about Generative AI preferences, including the way in which such preferences are treated, please read the Usage of my sounds for training generative AI models help section.
diff --git a/utils/search/backends/solr555pysolr.py b/utils/search/backends/solr555pysolr.py index 26e805e207..6a04b9b551 100644 --- a/utils/search/backends/solr555pysolr.py +++ b/utils/search/backends/solr555pysolr.py @@ -330,9 +330,6 @@ def convert_sound_to_search_engine_document(self, sound): document["spectral_path_l"] = locations["display"]["spectral"]["L"]["path"] document["preview_path"] = locations["preview"]["LQ"]["mp3"]["path"] - # Index uploader AI preference - document["gen_ai_preference"] = sound.user.profile.get_gen_ai_preference() - # Index consolidated audio descriptors descriptors_to_index = {} descriptors_data = sound.get_consolidated_analysis_data() @@ -381,6 +378,9 @@ def convert_sound_to_search_engine_document(self, sound): f"{settings.SEARCH_SOUNDS_FIELD_SUBCATEGORY}{SOLR_DYNAMIC_FIELDS_SUFFIX_MAP[settings.AUDIO_DESCRIPTOR_TYPE_STRING]}" ] = user_provided_subcategory + # Index uploader AI preference + document["gen_ai_preference"] = sound.get_gen_ai_preference() + # Finally add the sound ID and content type document.update({"id": sound.id, "content_type": SOLR_DOC_CONTENT_TYPES["sound"]}) From dc1015c1aa72cefd9446c74694745d938e20d0e2 Mon Sep 17 00:00:00 2001 From: ffont Date: Fri, 10 Jul 2026 08:57:01 +0200 Subject: [PATCH 07/10] Formatting --- freesound/audio_descriptor_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freesound/audio_descriptor_settings.py b/freesound/audio_descriptor_settings.py index 7814b2efbd..965abeda30 100644 --- a/freesound/audio_descriptor_settings.py +++ b/freesound/audio_descriptor_settings.py @@ -61,9 +61,9 @@ { "name": "bpm_confidence", "analyzer": "ac-extractor_v3", # "fs-essentia-extractor_v1", - "get_func": lambda d, s: ( - 1.0 if s.estimate_bpm_from_metadata() else d["tempo_confidence"] - ), # d["fs.bpm_confidence"], + "get_func": lambda d, s: 1.0 + if s.estimate_bpm_from_metadata() + else d["tempo_confidence"], # d["fs.bpm_confidence"], }, { "name": "brightness", From 2cd9283d5aaf643c1051c8a187e84d27f7a6d4af Mon Sep 17 00:00:00 2001 From: ffont Date: Fri, 10 Jul 2026 10:04:39 +0200 Subject: [PATCH 08/10] Further improvements in wording and option names for AI preferences --- _docs/api/source/resources.rst | 2 +- accounts/forms.py | 2 +- .../0044_alter_aipreference_preference.py | 35 +++++++++++++++++++ accounts/models.py | 16 ++++----- accounts/tests/test_profile.py | 4 +-- freesound/settings.py | 8 ++--- templates/accounts/edit_ai_preferences.html | 6 ++-- 7 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 accounts/migrations/0044_alter_aipreference_preference.py diff --git a/_docs/api/source/resources.rst b/_docs/api/source/resources.rst index 29c42722b8..117256d468 100644 --- a/_docs/api/source/resources.rst +++ b/_docs/api/source/resources.rst @@ -299,7 +299,7 @@ geotag string yes* Latitude and longitude o is_geotagged boolean yes Whether the sound has geotag information. created string yes The date when the sound was uploaded (e.g. "2014-04-16T20:07:11.145"). license string yes The Creative Commons license under which the sound is available to you ("Attribution", "Attribution NonCommercial", "Creative Commons 0"). -gen_ai_preference string yes The user preference regarding the use of the sound for training generative AI models ("no-additional-restrictions", "open-models", "open-noncommercial-models", "no-gen-ai"). Please check `our help section about generative AI training preferences `_ for more information about the meaning of these values. +gen_ai_preference string yes The user preference regarding the use of the sound for training generative AI models ("no-additional-preferences", "open-source-models", "noncommercial-open-source-models", "no-gen-ai"). Please check `our help section about generative AI training preferences `_ for more information about the meaning of these values. type string yes The original type of the sound (wav, aif, aiff, ogg, mp3, m4a, or flac). channels integer yes The number of sound channels (mostly 1 or 2). filesize integer yes The size of the file in bytes. diff --git a/accounts/forms.py b/accounts/forms.py index 5ce7503863..6418d55f22 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -498,7 +498,7 @@ class AIPreferenceForm(forms.Form): ) opt_out_speech = forms.BooleanField( label=mark_safe( - "Do not allow sounds classified under the \"Speech > Solo speech\" BST category to be used for the purpose of training Gen AI models." + "Express my preference that sounds classified under the \"Speech > Solo speech\" BST category not be used for the purpose of training Gen AI models." ), required=False, ) diff --git a/accounts/migrations/0044_alter_aipreference_preference.py b/accounts/migrations/0044_alter_aipreference_preference.py new file mode 100644 index 0000000000..9f15673837 --- /dev/null +++ b/accounts/migrations/0044_alter_aipreference_preference.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.27 on 2026-07-10 07:41 + +from django.db import migrations, models + + +def rename_aipreference_preferences(apps, schema_editor): + AIPreference = apps.get_model('accounts', 'AIPreference') + + preference_mapping = { + 'freesound-cc-recommendation': 'no-additional-preferences', + 'open-noncommercial-models': 'noncommercial-open-source-models', + 'open-models': 'open-source-models', + } + + for old_value, new_value in preference_mapping.items(): + AIPreference.objects.filter(preference=old_value).update(preference=new_value) + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0043_aipreference_opt_out_speech_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='aipreference', + name='preference', + field=models.CharField(choices=[('no-additional-preferences', 'No additional preferences'), ('open-source-models', 'Open Source Models only'), ('noncommercial-open-source-models', 'Non-Commercial Open Source Models only'), ('no-gen-ai', 'No generative AI')], default='no-additional-preferences'), + ), + migrations.RunPython( + rename_aipreference_preferences, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 36643c0a59..b7ba1ce1d5 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -747,27 +747,27 @@ class AIPreference(models.Model): AI_PREFERENCE_CHOICES_AND_EXPLANATION = [ { - "code": settings.AI_PREF_NO_ADDITIONAL_RESTRICTIONS, - "label": "No additional restrictions", - "explanation": """No additional statement, your sounds can be used for the purpose of training Gen AI models in accordance with the Creative Commons license terms. + "code": settings.AI_PREF_NO_ADDITIONAL_PREFERENCES, + "label": "No additional preferences", + "explanation": """No additional statement, your sounds remain subject to the Creative Commons license terms selected for them. Read here for more details.""", }, { "code": settings.AI_PREF_OPEN_MODELS, - "label": "Open Models only", - "explanation": """Your sounds can be used for the purpose of training Gen AI models in accordance with the Creative Commons license terms and as long as the trained models are made open source and freely available to the public. + "label": "Open Source Models only", + "explanation": """You express a preference for your sounds to be used for the purpose of training Gen AI models only when the trained models are released as Open Source Models and made freely available to the public. Read here for more details.""", }, { "code": settings.AI_PREF_OPEN_NONCOMMERCIAL_MODELS, - "label": "Open Non-Commercial Models only", - "explanation": """Your sounds can be used for the purpose of training Gen AI models in accordance with the Creative Commons license terms and as long as the trained models are made open source, freely available to the public, and not trained in a commercial setting nor used for commercial purposes. + "label": "Non-Commercial Open Source Models only", + "explanation": """You express a preference for your sounds to be used for the purpose of training Gen AI models only when the trained models are released as Open Source Models, made freely available to the public, and not trained in a commercial setting nor used for commercial purposes. Read here for more details.""", }, { "code": settings.AI_PREF_NO_GEN_AI, "label": "No generative AI", - "explanation": """Your sounds can not be used for the purpose of training Gen AI models. Read here for more details.""", + "explanation": """You express a preference that your sounds not be used for the purpose of training Gen AI models. Read here for more details.""", }, ] diff --git a/accounts/tests/test_profile.py b/accounts/tests/test_profile.py index ffa4793418..77f0fd3a58 100644 --- a/accounts/tests/test_profile.py +++ b/accounts/tests/test_profile.py @@ -224,7 +224,7 @@ def test_edit_user_profile_ai_preference(self): # Now user edits preference in profile page self.client.force_login(user) - new_preference = "open-models" + new_preference = "open-source-models" resp = self.client.post( "/home/ai-preferences/", { @@ -240,7 +240,7 @@ def test_edit_user_profile_ai_preference(self): self.assertEqual(Sound.objects.filter(user=user, is_index_dirty=True).count(), len(sounds)) # Now that there's an AI preference object already existing, try to change preference again and check that it works as expected - even_newer_preference = "no-additional-restrictions" + even_newer_preference = "no-additional-preferences" self.client.post( "/home/ai-preferences/", { diff --git a/freesound/settings.py b/freesound/settings.py index afcf21b7aa..8d06892fc9 100644 --- a/freesound/settings.py +++ b/freesound/settings.py @@ -671,11 +671,11 @@ def load_broad_sound_taxonomy_from_csv(path): # ------------------------------------------------------------------------------- # AI preferences panel -AI_PREF_NO_ADDITIONAL_RESTRICTIONS = "no-additional-restrictions" -AI_PREF_OPEN_MODELS = "open-models" -AI_PREF_OPEN_NONCOMMERCIAL_MODELS = "open-noncommercial-models" +AI_PREF_NO_ADDITIONAL_PREFERENCES = "no-additional-preferences" +AI_PREF_OPEN_MODELS = "open-source-models" +AI_PREF_OPEN_NONCOMMERCIAL_MODELS = "noncommercial-open-source-models" AI_PREF_NO_GEN_AI = "no-gen-ai" -DEFAULT_AI_PREFERENCE = AI_PREF_NO_ADDITIONAL_RESTRICTIONS +DEFAULT_AI_PREFERENCE = AI_PREF_NO_ADDITIONAL_PREFERENCES DEFAULT_OPT_OUT_SPEECH = False AI_OPT_OUT_SPEECH_TAXONOMY_CODES = ["sp-s"] # Solo-speech diff --git a/templates/accounts/edit_ai_preferences.html b/templates/accounts/edit_ai_preferences.html index b56e7b44c3..15f526032e 100644 --- a/templates/accounts/edit_ai_preferences.html +++ b/templates/accounts/edit_ai_preferences.html @@ -17,8 +17,8 @@

Generative AI preferences

This section allows you to set your preferences regarding the use of your uploaded sounds for the purpose of training generative Artificial Intelligence (Gen AI) models.

- Sounds may be used for Gen AI training in accordance with your chosen Creative Commons license terms. - You may choose to add a statement to express additional usage terms for the specific case of training Gen AI models, however there is no guarantee that these will have any legal effect. + The use of sounds for Gen AI training depends on your chosen Creative Commons license terms and how those terms are interpreted and applied. + You can add a statement to express your preferences for the specific case of training Gen AI models, however there is no guarantee that these will have any legal effect. Please, select one of the options below:

@@ -45,7 +45,7 @@
{{ ai_option.label }}
Complementarily to the options above, you can make an extra statement to opt-out from Gen AI model training for the particular case of voice recordings. - This option is designed to avoid the use of such sounds for training models that may generate speech that could be misused to impersonate real people. + This option is designed to express a preference against the use of such sounds for training models that may generate speech that could be misused to impersonate real people. Mark the checkbox below to enable this option:
From 246e6c306f73d148d8dad468b757d5145be175c8 Mon Sep 17 00:00:00 2001 From: ffont Date: Fri, 10 Jul 2026 10:05:16 +0200 Subject: [PATCH 09/10] Add missing migration from collections --- ...004_alter_collection_featured_sound_ids.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 fscollections/migrations/0004_alter_collection_featured_sound_ids.py diff --git a/fscollections/migrations/0004_alter_collection_featured_sound_ids.py b/fscollections/migrations/0004_alter_collection_featured_sound_ids.py new file mode 100644 index 0000000000..8f8721c167 --- /dev/null +++ b/fscollections/migrations/0004_alter_collection_featured_sound_ids.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.27 on 2026-07-10 08:04 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fscollections', '0003_remove_collectionsound_featured_sound_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='collection', + name='featured_sound_ids', + field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), blank=True, default=list, size=6), + ), + ] From dffc94ea831b0f9e69a3700926c181df4db1c448 Mon Sep 17 00:00:00 2001 From: ffont Date: Fri, 10 Jul 2026 10:15:03 +0200 Subject: [PATCH 10/10] Ruff formatting --- freesound/audio_descriptor_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freesound/audio_descriptor_settings.py b/freesound/audio_descriptor_settings.py index 965abeda30..7814b2efbd 100644 --- a/freesound/audio_descriptor_settings.py +++ b/freesound/audio_descriptor_settings.py @@ -61,9 +61,9 @@ { "name": "bpm_confidence", "analyzer": "ac-extractor_v3", # "fs-essentia-extractor_v1", - "get_func": lambda d, s: 1.0 - if s.estimate_bpm_from_metadata() - else d["tempo_confidence"], # d["fs.bpm_confidence"], + "get_func": lambda d, s: ( + 1.0 if s.estimate_bpm_from_metadata() else d["tempo_confidence"] + ), # d["fs.bpm_confidence"], }, { "name": "brightness",