Skip to content
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

Changes in Sound model and description form (BST and AI fields) #1808

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions accounts/tests/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ def test_describe_selected_files(self):
'0-license': '3',
'0-description': 'a test description for the sound file',
'0-new_pack': '',
'0-bst_category': 'ss',
'0-name': filenames[0],
'1-audio_filename': filenames[1],
'1-license': '3',
Expand All @@ -167,6 +168,8 @@ def test_describe_selected_files(self):
self.assertEqual(Pack.objects.filter(name='Name of a new pack').exists(), True)
self.assertEqual(Tag.objects.filter(name__contains="testtag").count(), 5)
self.assertNotEqual(user.sounds.get(original_filename=filenames[0]).geotag, None)
self.assertEqual(user.sounds.get(original_filename=filenames[0]).bst_category, 'ss')
self.assertEqual(user.sounds.get(original_filename=filenames[1]).bst_category, '')
sound_with_sources = user.sounds.get(original_filename=filenames[1])
self.assertEqual(sound_with_sources.sources.all().count(), len(sound_sources))

Expand Down
35 changes: 33 additions & 2 deletions apiv2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
###################

DEFAULT_FIELDS_IN_SOUND_LIST = 'id,name,tags,username,license' # Separated by commas (None = all)
DEFAULT_FIELDS_IN_SOUND_DETAIL = 'id,url,name,tags,description,geotag,created,license,type,channels,filesize,bitrate,' + \
DEFAULT_FIELDS_IN_SOUND_DETAIL = 'id,url,name,tags,description,bst_category,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,is_gen_ai' # All except for analyzers
DEFAULT_FIELDS_IN_PACK_DETAIL = None # Separated by commas (None = all)


Expand Down Expand Up @@ -100,6 +100,7 @@ class Meta:
'name',
'tags',
'description',
'bst_category',
'geotag',
'created',
'license',
Expand Down Expand Up @@ -131,6 +132,7 @@ class Meta:
'ac_analysis', # Kept for legacy reasons only as it is also contained in 'analyzers_output'
'analyzers_output',
'is_explicit',
'is_gen_ai',
'score',
)

Expand Down Expand Up @@ -311,6 +313,10 @@ def get_analyzers_output(self, obj):
is_explicit = serializers.SerializerMethodField()
def get_is_explicit(self, obj):
return obj.is_explicit

is_gen_ai = serializers.SerializerMethodField()
def get_is_gen_ai(self, obj):
return obj.is_gen_ai


class SoundListSerializer(AbstractSoundSerializer):
Expand Down Expand Up @@ -631,6 +637,12 @@ def validate_name(value):
return value


def validate_bst_category(value):
if value not in [key for key, name in Sound.BST_CATEGORY_CHOICES]:
raise serializers.ValidationError('Invalid BST category, should be a valid Broad Sound Taxonomy code.')
return value


def validate_tags(value):
tags = clean_and_split_tags(value)
if len(tags) < 3:
Expand Down Expand Up @@ -693,6 +705,8 @@ class SoundDescriptionSerializer(serializers.Serializer):
name = serializers.CharField(max_length=512, required=False,
help_text='Not required. Name you want to give to the sound (by default it will be '
'the original filename).')
bst_category = serializers.ChoiceField(required=False, allow_blank=True, choices=Sound.BST_CATEGORY_CHOICES,
help_text='Not required. Must be a valid Broad Sound Taxonomy category code.')
tags = serializers.CharField(max_length=512,
help_text='Separate tags with spaces. Join multi-word tags with dashes.')
description = serializers.CharField(help_text='Textual description of the sound.')
Expand Down Expand Up @@ -720,6 +734,9 @@ def validate_tags(self, value):

def validate_name(self, value):
return validate_name(value)

def validate_bst_category(self, value):
return validate_bst_category(value)

def validate_description(self, value):
return validate_description(value)
Expand All @@ -731,6 +748,8 @@ def validate_pack(self, value):
class EditSoundDescriptionSerializer(serializers.Serializer):
name = serializers.CharField(max_length=512, required=False,
help_text='Not required. New name you want to give to the sound.')
bst_category = serializers.ChoiceField(required=False, allow_blank=True, choices=Sound.BST_CATEGORY_CHOICES,
help_text='Not required. Must be a valid Broad Sound Taxonomy category code.')
tags = serializers.CharField(max_length=512, required=False,
help_text='Not required. Tags that should be assigned to the sound (note that '
'existing ones will be deleted). Separate tags with spaces. Join multi-word '
Expand All @@ -755,6 +774,9 @@ def validate_tags(self, value):

def validate_name(self, value):
return validate_name(value)

def validate_bst_category(self, value):
return validate_bst_category(value)

def validate_description(self, value):
return validate_description(value)
Expand All @@ -769,6 +791,8 @@ class UploadAndDescribeAudioFileSerializer(serializers.Serializer):
name = serializers.CharField(max_length=512, required=False,
help_text='Not required. Name you want to give to the sound (by default it will be '
'the original filename).')
bst_category = serializers.ChoiceField(required=False, allow_blank=True, choices=Sound.BST_CATEGORY_CHOICES,
help_text='Not required. Must be a valid Broad Sound Taxonomy category code.')
tags = serializers.CharField(max_length=512, required=False,
help_text='Only required if providing file description. Separate tags with spaces. '
'Join multi-word tags with dashes.')
Expand Down Expand Up @@ -808,15 +832,22 @@ def validate(self, data):
data['description'] = validate_description(self.initial_data.get('description', ''))
except serializers.ValidationError as e:
errors['description'] = e.detail

try:
data['name'] = validate_name(self.initial_data.get('name', ''))
except serializers.ValidationError as e:
errors['name'] = e.detail

try:
data['bst_category'] = validate_bst_category(self.initial_data.get('bst_category', ''))
except serializers.ValidationError as e:
errors['bst_category'] = e.detail

try:
data['tags'] = validate_tags(self.initial_data.get('tags', ''))
except serializers.ValidationError as e:
errors['tags'] = e.detail

try:
data['geotag'] = validate_geotag(self.initial_data.get('geotag', ''))
except serializers.ValidationError as e:
Expand Down
3 changes: 3 additions & 0 deletions apiv2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,9 @@ def post(self, request, *args, **kwargs):
if 'name' in serializer.data:
if serializer.data['name']:
sound.original_filename = serializer.data['name']
if 'bst_category' in serializer.data:
if serializer.data['bst_category']:
sound.bst_category = serializer.data['bst_category']
if 'description' in serializer.data:
if serializer.data['description']:
sound.description = serializer.data['description']
Expand Down
11 changes: 11 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,17 @@ services:
depends_on:
- rabbitmq

# BST analyzer - needs image to be built from freesound-audio-analyzers repository
worker_analyzer6:
profiles: ["analyzers", "all"]
image: bst-extractor_v1
volumes:
- ./freesound-data/:/freesound-data
init: true
command: celery -A main worker --pool=threads --concurrency=1 -l info -Q bst-extractor_v1
depends_on:
- rabbitmq

# Similarity http server
similarity:
profiles: ["all"]
Expand Down
39 changes: 39 additions & 0 deletions freesound/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,45 @@

ANNOUNCEMENT_CACHE_KEY = 'announcement_cache'

# -------------------------------------------------------------------------------
# Broad Sound Taxonomy definition

BROAD_SOUND_TAXONOMY = [
{'category_code': 'm', 'level': 1, 'name': 'Music', 'description': 'Music excerpts, melodies, loops, fillers, drones and short musical snippets.'},
{'category_code': 'm-sp', 'level': 2, 'name': 'Solo percussion', 'description': 'Music excerpts with solo percussive instruments.'},
{'category_code': 'm-si', 'level': 2, 'name': 'Solo instrument', 'description': 'Music excerpts with only one instrument, excluding percussion.'},
{'category_code': 'm-m', 'level': 2, 'name': 'Multiple instruments', 'description': 'Music excerpts with more than one instrument.'},
{'category_code': 'm-other', 'level': 2, 'name': 'Other', 'description': 'Music that doesn\'t belong to any of the above categories.'},
{'category_code': 'is', 'level': 1, 'name': 'Instrument samples', 'description': 'Single notes from musical instruments, various versions of the same note, and scales.'},
{'category_code': 'is-p', 'level': 2, 'name': 'Percussion', 'description': 'Instrument samples that are percussive (idiophones or membraphones).'},
{'category_code': 'is-s', 'level': 2, 'name': 'String', 'description': 'Instrument samples that belong to the string instrument family.'},
{'category_code': 'is-w', 'level': 2, 'name': 'Wind', 'description': 'Instrument samples that belong to the wind instrument family (aerophones).'},
{'category_code': 'is-k', 'level': 2, 'name': 'Piano / Keyboard instruments', 'description': 'Instrument samples of piano or other keyboard instruments, not synthesized.'},
{'category_code': 'is-e', 'level': 2, 'name': 'Synths / Electronic', 'description': 'Instrument samples synthesized or produced by electronic means.'},
{'category_code': 'is-other', 'level': 2, 'name': 'Other', 'description': 'Instrument samples that don\'t belong to any of the above categories.'},
{'category_code': 'sp', 'level': 1, 'name': 'Speech', 'description': 'Sounds where human voice is prominent.'},
{'category_code': 'sp-s', 'level': 2, 'name': 'Solo speech', 'description': 'Recording of a single voice speaking, excluding singing.'},
{'category_code': 'sp-c', 'level': 2, 'name': 'Conversation / Crowd', 'description': 'Several people talking, having a conversation or dialogue.'},
{'category_code': 'sp-p', 'level': 2, 'name': 'Processed / Synthetic', 'description': 'Voice(s) from an indirect source (e.g. radio), processed or synthesized.'},
{'category_code': 'sp-other', 'level': 2, 'name': 'Other', 'description': 'Voice-predominant recordings that don\'t belong to any of the above categories.'},
{'category_code': 'fx', 'level': 1, 'name': 'Sound effects', 'description': 'Isolated sound effects or sound events, each happening one at a time.'},
{'category_code': 'fx-o', 'level': 2, 'name': 'Objects / House appliances', 'description': 'Everyday objects, inside the home or smaller in size.'},
{'category_code': 'fx-v', 'level': 2, 'name': 'Vehicles', 'description': 'Sounds produced from a vehicle.'},
{'category_code': 'fx-m', 'level': 2, 'name': 'Other mechanisms, engines, machines', 'description': 'Machine-like sounds, except vehicles and small house electric devices.'},
{'category_code': 'fx-h', 'level': 2, 'name': 'Human sounds and actions', 'description': 'Sounds from the human body, excluding speech.'},
{'category_code': 'fx-a', 'level': 2, 'name': 'Animals', 'description': 'Animal vocalizations or sounds.'},
{'category_code': 'fx-n', 'level': 2, 'name': 'Natural elements and explosions', 'description': 'Sound events occuring by natural processes.'},
{'category_code': 'fx-ex', 'level': 2, 'name': 'Experimental', 'description': 'Experimental sounds or heavily processed audio recordings.'},
{'category_code': 'fx-el', 'level': 2, 'name': 'Electronic / Design', 'description': 'Sound effects that are computer-made or designed for user interfaces or animations.'},
{'category_code': 'fx-other', 'level': 2, 'name': 'Other', 'description': 'Sound effects that don\'t belong to any of the above categories.'},
{'category_code': 'ss', 'level': 1, 'name': 'Soundscapes', 'description': 'Ambiances, field-recordings with multiple events and sound environments.'},
{'category_code': 'ss-n', 'level': 2, 'name': 'Nature', 'description': 'Soundscapes from natural habitats.'},
{'category_code': 'ss-i', 'level': 2, 'name': 'Indoors', 'description': 'Soundscapes from closed or indoor spaces.'},
{'category_code': 'ss-u', 'level': 2, 'name': 'Urban', 'description': 'Soundscapes from cityscapes or outdoor places with human intervention.'},
{'category_code': 'ss-s', 'level': 2, 'name': 'Synthetic / Artificial', 'description': 'Soundscapes that are synthesized or computer-made ambiences.'},
{'category_code': 'ss-other', 'level': 2, 'name': 'Other', 'description': 'Soundscapes that don\'t belong to any of the above categories.'},
]

# -------------------------------------------------------------------------------
# Freesound data paths and urls

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const updateSubcategoriesList = (hiddenField, subcategoryContainer, subcategoryButtons) => {
if (hiddenField.value === "") {
subcategoryContainer.classList.add("display-none");
return;
} else {
// Display subcategories that match the selected top-level category
// Triggered when a selection is made or updated.
const correspondingTopLevelValue = hiddenField.value.split("-")[0]; // Extract top-level value, if not already.
if(correspondingTopLevelValue) {
subcategoryContainer.classList.remove("display-none");
subcategoryButtons.forEach(subBtn => {
const topLevelValue = subBtn.getAttribute("top_level");
subBtn.style.display = topLevelValue === correspondingTopLevelValue ? "inline-block" : "none";
});
}
}
}

const prepareCategoryFormFields = (mainContainer) => {
const categoryFieldContainers = mainContainer.getElementsByClassName("bst-category-field");
categoryFieldContainers.forEach(container => {
const hiddenField = container.querySelectorAll("input[type=hidden]")[0];
const topButtons = container.querySelectorAll(".top-buttons .btn");
const subcategoryContainer = container.querySelector(".subcategory-buttons");
const subcategoryButtons = subcategoryContainer.querySelectorAll(".btn-subcategory");


// Event listener for top-level category buttons
topButtons.forEach(topBtn => {
topBtn.addEventListener("click", function () {
const selectedValue = this.getAttribute("data_value");

// Update hidden input value
hiddenField.value = selectedValue;

// Highlight the selected top-level category button
topButtons.forEach(btn => {
btn.classList.remove("btn-primary")
btn.classList.add("btn-inverse")
btn.setAttribute("aria-selected", "false")
})
this.classList.remove("btn-inverse")
this.classList.add("btn-primary")
this.setAttribute("aria-selected", "true")

updateSubcategoriesList(hiddenField, subcategoryContainer, subcategoryButtons);
});
});

// Event listener for subcategory buttons
subcategoryButtons.forEach(subBtn => {
subBtn.addEventListener("click", function () {
const subcategoryValue = this.getAttribute("data_value");

// Update hidden input value if subcategory is clicked
hiddenField.value = subcategoryValue;

// Highlight the selected subcategory button
subcategoryButtons.forEach(btn => {
btn.classList.remove("btn-primary")
btn.classList.add("btn-inverse")
btn.setAttribute("aria-selected", "false")
})
this.classList.remove("btn-inverse")
this.classList.add("btn-primary")
btn.setAttribute("aria-selected", "true");
});
});

updateSubcategoriesList(hiddenField, subcategoryContainer, subcategoryButtons);
});
}

export {prepareCategoryFormFields}
6 changes: 5 additions & 1 deletion freesound/static/bw-frontend/src/pages/editDescribeSounds.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {prepareTagsFormFields, updateTags} from "../components/tagsFormField"
import {prepareGeotagFormFields} from "../components/geotagFormField"
import {preparePackFormFields} from "../components/packFormField"
import {prepareAddSoundsModalAndFields} from "../components/addSoundsModal"
import {prepareCategoryFormFields} from "../components/bstCategoryFormField";

prepareAddSoundsModalAndFields(document);
prepareTagsFormFields(document);
preparePackFormFields(document);
prepareCategoryFormFields(document);
document.addEventListener("DOMContentLoaded", () => {
// Running this inside DOMContentLoaded to make sure mapbox gl scripts are loaded
prepareGeotagFormFields(document);
Expand All @@ -28,4 +30,6 @@ inputTypeSubmitElements.forEach(button => {
}
});
});
});
});

// Move json for BST category field in description form here
3 changes: 2 additions & 1 deletion freesound/static/bw-frontend/styles/atoms/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
@import 'selectableObject';
@import 'remixArrows';
@import 'table';
@import 'announcementBanner';
@import 'announcementBanner';
@import 'tooltip';
Loading
Loading