Skip to content
Merged
2 changes: 1 addition & 1 deletion _docs/api/source/resources.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://freesound.org/help/faq/#can-my-sounds-be-used-to-train-artificial-intelligence-ai-models>`_ 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 <https://freesound.org/help/faq/#can-my-sounds-be-used-to-train-artificial-intelligence-ai-models>`_ 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.
Expand Down
37 changes: 20 additions & 17 deletions accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'<div class="v-spacing-1 text-grey" id="ai-section">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="/help/faq/#can-my-sounds-be-used-to-train-generative-artificial-intelligence-gen-ai-models">'
"<i>Usage of my sounds for "
"training generative 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
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 @@ -507,6 +490,26 @@ 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(),
)
opt_out_speech = forms.BooleanField(
label=mark_safe(
"Express my preference that sounds classified under the <a href='/help/broad-sound-taxonomy'>\"Speech &gt; Solo speech\" BST category </a> not 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):
email = forms.EmailField(label="New email address", max_length=254)
password = forms.CharField(label="Your password", widget=forms.PasswordInput)
Expand Down
23 changes: 23 additions & 0 deletions accounts/migrations/0043_aipreference_opt_out_speech_and_more.py
Original file line number Diff line number Diff line change
@@ -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'),
),
]
35 changes: 35 additions & 0 deletions accounts/migrations/0044_alter_aipreference_preference.py
Original file line number Diff line number Diff line change
@@ -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,
),
]
75 changes: 57 additions & 18 deletions accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -695,17 +695,39 @@ 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, 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_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",)
Expand All @@ -722,19 +744,36 @@ 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",
),
)
DEFAULT_AI_PREFERENCE = "freesound-cc-recommendation"
preference = models.CharField(choices=AI_PREFERENCE_CHOICES, default=DEFAULT_AI_PREFERENCE)

AI_PREFERENCE_CHOICES_AND_EXPLANATION = [
{
"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.
<a href="/help/faq/#can-my-sounds-be-used-to-train-generative-artificial-intelligence-gen-ai-models">Read here for more details</a>.""",
},
{
"code": settings.AI_PREF_OPEN_MODELS,
"label": "Open Source Models only",
"explanation": """You express a preference for your sounds to be used for the purpose of training Gen AI models <b>only when the trained models are released as Open Source Models and made freely available to the public</b>.
<a href="/help/faq/#can-my-sounds-be-used-to-train-generative-artificial-intelligence-gen-ai-models">Read here for more details</a>.""",
},
{
"code": settings.AI_PREF_OPEN_NONCOMMERCIAL_MODELS,
"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 <b>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</b>.
<a href="/help/faq/#can-my-sounds-be-used-to-train-generative-artificial-intelligence-gen-ai-models">Read here for more details</a>.""",
},
{
"code": settings.AI_PREF_NO_GEN_AI,
"label": "No generative AI",
"explanation": """You express a preference that your sounds not be used for the purpose of training Gen AI models. <a href="/help/faq/#can-my-sounds-be-used-to-train-generative-artificial-intelligence-gen-ai-models">Read here for more details</a>.""",
},
]

AI_PREFERENCE_CHOICES = [(item["code"], item["label"]) for item in AI_PREFERENCE_CHOICES_AND_EXPLANATION]
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):
Expand Down
41 changes: 18 additions & 23 deletions accounts/tests/test_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -216,46 +216,41 @@ 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)
self.assertEqual(user.profile.get_gen_ai_preference_opt_out_speech(), settings.DEFAULT_OPT_OUT_SPEECH)

# 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(), settings.DEFAULT_AI_PREFERENCE)

# Now user edits preference in profile page
self.client.force_login(user)
new_preference = "open-models"
self.client.post(
"/home/edit/",
new_preference = "open-source-models"
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,
"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_ai_preference(), new_preference)
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
even_newer_preference = "freesound-cc-recommendation"
even_newer_preference = "no-additional-preferences"
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,
"opt_out_speech": False,
},
)
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)
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")
Expand Down
1 change: 1 addition & 0 deletions accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Loading
Loading