Repository: felixbur/nkululeko (language: python)

Issue:
[BUG] Finetuning cannot accept `balancing` keys
**Describe the bug and step to reproduce**
Add `balancing = smote` to [FEATS] with model type = finetune, the following error will occurs:

```
...
DEBUG: modelrunner: balancing the training features with: smote
Traceback (most recent call last):
  File "/home/bagus/miniconda3/envs/nkululeko/lib/python3.9/runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/home/bagus/miniconda3/envs/nkululeko/lib/python3.9/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/home/bagus/github/nkululeko/nkululeko/nkululeko.py", line 73, in <module>
    main(cwd)  # use this if you want to state the config file path on command line
  File "/home/bagus/github/nkululeko/nkululeko/nkululeko.py", line 68, in main
    doit(config_file)
  File "/home/bagus/github/nkululeko/nkululeko/nkululeko.py", line 53, in doit
    reports, last_epochs = expr.run()
  File "/home/bagus/github/nkululeko/nkululeko/experiment.py", line 614, in run
    self.runmgr.do_runs()
  File "/home/bagus/github/nkululeko/nkululeko/runmanager.py", line 56, in do_runs
    self.modelrunner = Modelrunner(
  File "/home/bagus/github/nkululeko/nkululeko/modelrunner.py", line 32, in __init__
    self._select_model(model_type)
  File "/home/bagus/github/nkululeko/nkululeko/modelrunner.py", line 132, in _select_model
    self._check_balancing()
  File "/home/bagus/github/nkululeko/nkululeko/modelrunner.py", line 249, in _check_balancing
    X_res, y_res = sampler.fit_resample(
  File "/home/bagus/miniconda3/envs/nkululeko/lib/python3.9/site-packages/imblearn/base.py", line 208, in fit_resample
    return super().fit_resample(X, y)
  File "/home/bagus/miniconda3/envs/nkululeko/lib/python3.9/site-packages/imblearn/base.py", line 106, in fit_resample
    X, y, binarize_y = self._check_X_y(X, y)
  File "/home/bagus/miniconda3/envs/nkululeko/lib/python3.9/site-packages/imblearn/base.py", line 161, in _check_X_y
    X, y = self._validate_data(X, y, reset=True, accept_sparse=accept_sparse)
  File "/home/bagus/miniconda3/envs/nkululeko/lib/python3.9/site-packages/sklearn/base.py", line 621, in _validate_data
    X, y = check_X_y(X, y, **check_params)
  File "/home/bagus/miniconda3/envs/nkululeko/lib/python3.9/site-packages/sklearn/utils/validation.py", line 1147, in check_X_y
    X = check_array(
  File "/home/bagus/miniconda3/envs/nkululeko/lib/python3.9/site-packages/sklearn/utils/validation.py", line 797, in check_array
    dtype_orig = np.result_type(*dtypes_orig)
  File "<__array_function__ internals>", line 200, in result_type
ValueError: at least one array or dtype is required
```

**Expected behavior**
No errors

**Desktop information**
 - OS:  Ubuntu 20.04
 - Nkululeko Version: 0.89.2

Create python script to automate supported dataset information
The readme in under data directory is created manually to summarize data information.

It can be generated automatically using python.
input: README.md files from each dataset directory
output: README.md under data directory to summmarize all suported datasets

Option for header/column:  
name: dataset/directory name
target: labels
access: public, private, or restricted
content: short description

optional
number of utterance: count number of lines from train, test, dev CSV files. I don't know for audformat
languages:  
UA:
WA: 




Discussion / hints:

 cheers @bagustris , makes a lot of sense to me

So that would require some  formal data.description per database folder? e.g in Json or Python directly?
or perhaps yaml (currently looking into hydra)
@felixbur 

Any solution would be fine for the first attempt. The `data` directory in our nkululeko reflects `recipe` or `egs` (in style of Kaldi and ESPNET). It will grow larger and larger, so I think it needs automatic processing to update itself (or there is no need?)

I am just motivated by the script below,

https://github.com/SuperKogito/SER-datasets/blob/master/src/generate_files.py

Then, use GitHub-action to make an automatic run and update on every push.

Maybe Hydra provides a better solution (never use it)? 

Fix patch:
diff --git a/.github/workflows/format_code.yml b/.github/workflows/format_code.yml
new file mode 100644
index 00000000..5f14374c
--- /dev/null
+++ b/.github/workflows/format_code.yml
@@ -0,0 +1,12 @@
+name: Check code formatting
+
+on: [push, pull_request]
+
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: psf/black@stable
+      # - uses: psf/black@552baf822992936134cbd31a38f69c8cfe7c0f05
+  
diff --git a/.github/workflows/isort.yaml b/.github/workflows/isort.yaml
new file mode 100644
index 00000000..5961a1b9
--- /dev/null
+++ b/.github/workflows/isort.yaml
@@ -0,0 +1,16 @@
+name: Run isort
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: isort/isort-action@v1
+      # - uses: isort/isort-action@master
+        with:
+            # isortVersion: 5.13.2
+            sortPaths: 'nkululeko'
+            configuration: '--profile black'
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5fe9f2f2..06b59276 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,7 +21,7 @@ The preferred way to contribute to nkululeko is to fork the [main repository](ht
 
 	```bash
 	git clone https://github.com/YourLogin/nkululeko.git
-	cd spafe
+	cd nkululeko
 	```
 
 3.	Remove any previously installed nkululeko versions, then install your local copy with testing dependencies:
@@ -43,9 +43,14 @@ The preferred way to contribute to nkululeko is to fork the [main repository](ht
 	-> Please never work directly on the `master` branch!
 	```
 
-6.	Once you are done, make sure to format the code using black to fit spafe's codestyle.
+6.	Once you are done, make sure to format the code using black to fit Nkululeko's codestyle.
 
-	```black nkululeko/```
+	```bash
+	black nkululeko/
+	isort --profile black nkululeko/
+	# Alternatively and additionaly, use ruff:
+	ruff check --fix --output-format=full nkululeko
+	```
 
 7.	Make sure that the tests succeed and have enough coverage.
 
diff --git a/data/README.md b/data/README.md
index 603c9fac..0effab7c 100644
--- a/data/README.md
+++ b/data/README.md
@@ -5,7 +5,7 @@ Nkululeko database repository
 # Data
 
 
-This is the default top directory for Nkululeko data import.Each database should be in its own subfolder (you can also use `ln -sf`` to soft link original database path to these subfolders) and contain a README how to import the data to Nkululeko CSV or audformat.
+This is the default top directory for Nkululeko data import. Each database should be in its own subfolder (you can also use `ln -sf` to soft link original database path to these subfolders) and contain a README how to import the data to Nkululeko CSV or audformat.
 ## Accessibility
 
 
@@ -15,12 +15,10 @@ The column `access` in the table below indicates the database's accessability. T
 - `private`: the database is not publicly available on the internet and requires the private information of the owner of the dataset.
 
 
-To support open science and reproducible research, we only accept PR and recipes for public dataset for now on.
-## Databases
-
+To support open science and reproducible research, we encourage to submit PR and recipes for public dataset for now on.
 |Name|Target|Description|Access|
 | :--- | :--- | :--- | :--- |
-|emorynlp|emotion|English, From Friends TV|public|
+|emorynlp|emotion|English Emotion Dataset from Friends TV Show|public|
 |emns|emotion,intensity|British, singles peaker, UAR=.479|public|
 |test|none|Test data for nkululeko|public|
 |catsvsdogs|cats_dogs|kaggle test set|public|
@@ -72,11 +70,13 @@ To support open science and reproducible research, we only accept PR and recipes
 |urdu|emotion|Urdu|public|
 |polish|emotion|Polish|public|
 |cmu-mosei|sentiment,emotion|English, original link dead|public|
-|SVD|pathologicalspeech|German|public|
+|svd|pahtological speech|German speech data for detecting various pathological voices|public|
 |msp-improv|emotion,VAD,naturalness|English|restricted|
 |shemo|emotion|Persian|public|
 |esd|emotion|English,Chinese|public|
 
+
+This recipe contains information about 56 datasets.
 ## Performance
   
 ![Nkululeko performance](../meta/images/nkululeko_ser_20240719.png)  
diff --git a/data/androids/process_database.py b/data/androids/process_database.py
index 568910a9..632de83f 100644
--- a/data/androids/process_database.py
+++ b/data/androids/process_database.py
@@ -15,9 +15,10 @@
 
 """
 
-import pandas as pd
 import os
+
 import audeer
+import pandas as pd
 
 dataset_name = 'androids'
 data_root = './Androids-Corpus/'
diff --git a/data/banglaser/process_database.py b/data/banglaser/process_database.py
index 6ddf957e..14c6e7d3 100644
--- a/data/banglaser/process_database.py
+++ b/data/banglaser/process_database.py
@@ -14,12 +14,14 @@
 GG = Actor ID, 01-34 (odd: male, even: female)
 """
 
-import pandas as pd
-from nkululeko.utils.files import find_files
 import argparse
-from sklearn.model_selection import train_test_split
 from pathlib import Path
 
+import pandas as pd
+from sklearn.model_selection import train_test_split
+
+from nkululeko.utils.files import find_files
+
 
 def process_database(data_dir, output_dir):
     # check if data_dir exists
diff --git a/data/crema-d/load_db.py b/data/crema-d/load_db.py
index ba60852e..cb83290a 100644
--- a/data/crema-d/load_db.py
+++ b/data/crema-d/load_db.py
@@ -2,7 +2,6 @@
 
 import audb
 
-
 # set download directory to current
 cwd = os.getcwd()
 audb.config.CACHE_ROOT = cwd
diff --git a/data/descr.yml b/data/descr.yml
new file mode 100644
index 00000000..047f9edf
--- /dev/null
+++ b/data/descr.yml
@@ -0,0 +1,397 @@
+# EMORYNLP Dataset
+EMORYLP:
+- name: emorynlp
+- target: emotion
+- descr: English Emotion Dataset from Friends TV Show
+- access: public
+
+# EMNS Dataset
+EMNS:
+- name: emns
+- target: emotion,intensity
+- descr: British, singles peaker, UAR=.479
+- access: public
+
+# Test Dataset
+TEST:
+- name: test
+- target: none
+- descr: Test data for nkululeko
+- access: public
+
+# Cats vs Dogs Dataset
+CATSVSDOGS:
+- name: catsvsdogs
+- target: cats_dogs
+- descr: kaggle test set
+- access: public
+
+# CLAC Dataset
+CLAC:
+- name: clac
+- target: healthyspeech,age,gender
+- descr: English
+- access: public
+
+# VIVAE Dataset
+VIVAE:
+- name: vivae
+- target: emotion
+- descr: English vocal bursts
+- access: public
+
+# EmoFilm Dataset
+EMOFILM:
+- name: emofilm
+- target: emotion
+- descr: English, Spanish, Italian
+- access: restricted
+
+# Emozionalmente Dataset
+EMOZIONALMENTE:
+- name: emozionalmente
+- target: emotion
+- descr: Italian
+- access: public
+
+# Laughter Types Dataset
+LAUGHTERTYPES:
+- name: laughter-types
+- target: laughter
+- descr: Master Thesis from TUBerlin
+- access: public
+
+# SAVEE Dataset
+SAVEE:
+- name: savee
+- target: emotion
+- descr: English, from tfds
+- access: restricted
+
+# EMOVO Dataset
+EMOVO:
+- name: emovo
+- target: emotion
+- descr: Italian
+- access: restricted
+
+# SUBESCO Dataset
+SUBESCO:
+- name: subesco
+- target: emotion
+- descr: Bangla
+- access: public
+
+# OREAU2 Dataset
+OREAU2:
+- name: oreau2
+- target: emotion
+- descr: French
+- access: public
+
+# MESS Dataset
+MESS:
+- name: mess
+- target: emotion
+- descr: English
+- access: public
+
+# EMOV-DB Dataset
+EMOVDB:
+- name: emov-db
+- target: emotion
+- descr: English
+- access: public
+
+# Odyssey CAT 2024 Dataset
+ODYSSEYCAT2024:
+- name: odyssey-cat-2024
+- target: emotion
+- descr: Data for Odyssey 2024 challenge, needs MSPPodcast
+- access: restricted
+
+# RAVDESS Dataset
+RAVDESS:
+- name: ravdess
+- target: emotion,speaker
+- descr: English
+- access: public
+
+# ERYSAC Dataset
+ERYSAC:
+- name: erysac
+- target: emotion
+- descr: Russian, children
+- access: public
+
+# DEMOS Dataset
+DEMOS:
+- name: demos
+- target: emotion
+- descr: Italian
+- access: restricted
+
+# EMO-DB Dataset
+EMODB:
+- name: emodb
+- target: emotion
+- descr: German
+- access: public
+
+# JNV Dataset
+JNV:
+- name: jnv
+- target: emotion
+- descr: Japanese, non-verbals
+- access: public
+
+# AESDD Dataset
+AESDD:
+- name: aesdd
+- target: emotion
+- descr: amharic language
+- access: public
+
+# MSP-Podcast Dataset
+MSPPODCAST:
+- name: msp-podcast
+- target: emotion,VAD
+- descr: English
+- access: restricted
+
+# BAVED Dataset
+BAVED:
+- name: baved
+- target: emotion
+- descr: Arabic
+- access: public
+
+# ASVP-ESD Dataset
+ASVPESD:
+- name: asvp-esd
+- target: emotion
+- descr: Multilingual, also contain vocal bursts
+- access: public
+
+# Estonian Emotional Speech Corpus Dataset
+EKORPUS:
+- name: ekorpus
+- target: emotion
+- descr: Estonian
+- access: public
+
+# IEMOCAP Dataset
+IEMOCAP:
+- name: iemocap
+- target: emotion,VAD
+- descr: English
+- access: restriced
+
+# JL Dataset
+JL:
+- name: jl
+- target: emotion
+- descr: English
+- access: public
+
+# SYNTACT Dataset
+SYNTACT:
+- name: syntact
+- target: emotion
+- descr: Synthesized German speech
+- access: public
+
+# KIA Dataset
+KIA:
+- name: kia
+- target: emotion
+- descr: Korean, wake-up word
+- access: public
+
+# Portuguese Dataset
+PORTUGUESE:
+- name: portuguese
+- target: emotion
+- descr: Portuguese
+- access: public
+
+# CREMA-D Dataset
+CREMAD:
+- name: crema-d
+- target: emotion
+- descr: English,adopted from tfds
+- access: public
+
+# Thorsten Emotional Dataset
+THORSTENEMOTIONAL:
+- name: thorsten-emotional
+- target: emotion
+- descr: German
+- access: public
+
+# JVNV Dataset
+JVNV:
+- name: jvnv
+- target: emotion
+- descr: Japanese, verbal and non-verbal
+- access: public
+
+# MESD Dataset
+MESD:
+- name: mesd
+- target: emotion
+- descr: Mexican
+- access: public
+
+# eNTERFACE Dataset
+ENTERFACE:
+- name: enterface
+- target: emotion
+- descr: Multilingual
+- access: public
+
+# TUREV Dataset
+TUREV:
+- name: turev
+- target: emotion
+- descr: Turkish
+- access: public
+
+# nEMO Dataset
+NEMO:
+- name: nEMO
+- target: emotion,VAD
+- descr: Polish
+- access: public
+
+# EAED Dataset
+EAED:
+- name: eaed
+- target: emotion
+- descr: Arabic
+- access: public
+
+# ASED Dataset
+ASED:
+- name: ased
+- target: emotion
+- descr: Greek
+- access: public
+
+# CAFE Dataset
+CAFE:
+- name: cafe
+- target: emotion
+- descr: Childrenspeech, CanadianFrench
+- access: public
+
+# BanglaSER Dataset
+BANGLASER:
+- name: banglaser
+- target: emotion
+- descr: Bengali
+- access: public
+
+# MLENDSND Dataset
+MLENDSND:
+- name: mlendsnd
+- target: emotion
+- descr: English
+- access: public
+
+# GERPARAS Dataset
+GERPARAS:
+- name: gerparas
+- target: valence,arousal,dominance
+- descr: German
+- access: restricted
+
+# Androids Dataset
+ANDROIDS:
+- name: androids
+- target: depression
+- descr: English
+- access: public
+
+# KBES Dataset
+KBES:
+- name: kbes
+- target: emotion
+- descr: Bengali
+- access: public
+
+# TESS Dataset
+TESS:
+- name: tess
+- target: emotion
+- descr: British English (Toronto)
+- access: public
+
+# JTES Dataset
+JTES:
+- name: jtes
+- target: emotion
+- descr: Japanese
+- access: private
+
+# MELD Dataset
+MELD:
+- name: meld
+- target: emotion
+- descr: English, From Friends TV
+- access: public
+
+# Urdu Dataset
+URDU:
+- name: urdu
+- target: emotion
+- descr: Urdu
+- access: public
+
+# Polish Dataset
+POLISH:
+- name: polish
+- target: emotion
+- descr: Polish
+- access: public
+
+# CMU-MOSEI Dataset
+CMUMOSEI:
+- name: cmu-mosei
+- target: sentiment,emotion
+- descr: English, original link dead
+- access: public
+
+# SVD Dataset
+SVD:
+- name: SVD
+- target: pathologicalspeech
+- descr: German
+- access: public
+
+# MSP-IMPROV Dataset
+MSPIMPROV:
+- name: msp-improv
+- target: emotion,VAD,naturalness
+- descr: English
+- access: restricted
+
+# ShEMO Dataset
+SHEMO:
+- name: shemo
+- target: emotion
+- descr: Persian
+- access: public
+
+# ESD Dataset
+ESD:
+- name: esd
+- target: emotion
+- descr: English,Chinese
+- access: public
+
+SVD:
+- name: svd
+- target: pahtological speech
+- descr: German speech data for detecting various pathological voices
+- access: public
\ No newline at end of file
diff --git a/data/eaed/process_database.py b/data/eaed/process_database.py
index 4238a1e2..8d95cf0e 100644
--- a/data/eaed/process_database.py
+++ b/data/eaed/process_database.py
@@ -3,10 +3,12 @@
 file name format: <speaker>_<emotion>_<(number)>.wav
 """
 
-import pandas as pd
+import argparse
 from pathlib import Path
+
+import pandas as pd
 from sklearn.model_selection import train_test_split
-import argparse
+
 from nkululeko.utils.files import find_files
 
 
diff --git a/data/emofilm/convert_to_16k.py b/data/emofilm/convert_to_16k.py
index a2acdfd5..f8603985 100644
--- a/data/emofilm/convert_to_16k.py
+++ b/data/emofilm/convert_to_16k.py
@@ -2,9 +2,9 @@
 # arguments: input_dir, output_dir
 # sox must be installed
 
+import argparse
 import os
 import subprocess
-import argparse
 
 parser = argparse.ArgumentParser()
 parser.add_argument('-i', '--input_dir', type=str, default='./EmoFilm/wav_corpus')
diff --git a/data/emofilm/process_database.py b/data/emofilm/process_database.py
index bf5f08e1..362ca3c9 100644
--- a/data/emofilm/process_database.py
+++ b/data/emofilm/process_database.py
@@ -1,10 +1,11 @@
 # process_database.py -> EmoFilm database
 
+import argparse
 import os
+
+import numpy as np
 import pandas as pd
 from sklearn.model_selection import train_test_split
-import numpy as np
-import argparse
 
 # load data
 
diff --git a/data/emov-db/process_database.py b/data/emov-db/process_database.py
index a631566b..9d950831 100644
--- a/data/emov-db/process_database.py
+++ b/data/emov-db/process_database.py
@@ -7,6 +7,7 @@
 from pathlib import Path
 
 import pandas as pd
+
 from nkululeko.utils import files
 
 emotion_map = {
diff --git a/data/emozionalmente/create.py b/data/emozionalmente/create.py
index c332290e..ffd1854f 100644
--- a/data/emozionalmente/create.py
+++ b/data/emozionalmente/create.py
@@ -6,11 +6,10 @@
 import os
 import shutil
 
-import numpy as np
-import pandas as pd
-
 import audeer
 import audformat
+import numpy as np
+import pandas as pd
 
 
 def main():
diff --git a/data/erysac/process_database.py b/data/erysac/process_database.py
index da646421..7b0a4696 100644
--- a/data/erysac/process_database.py
+++ b/data/erysac/process_database.py
@@ -2,9 +2,11 @@
 
 import argparse
 from pathlib import Path
-from nkululeko.utils.files import find_files
+
 import pandas as pd
 
+from nkululeko.utils.files import find_files
+
 gender_map = {"m": "male", "f": "female"}
 
 def process_database(data_dir, output_dir):
diff --git a/data/jvnv/process_database.py b/data/jvnv/process_database.py
index b94e82c1..fbe2bbc1 100644
--- a/data/jvnv/process_database.py
+++ b/data/jvnv/process_database.py
@@ -1,9 +1,10 @@
 # process_database.py: for JVNV database
 # export PYTHONPATH="${PYTHONPATH}:/home/bagus/github/nkululeko"
 
-import os
-import csv
 import argparse
+import csv
+import os
+
 from nkululeko.utils.files import find_files
 
 
diff --git a/data/kbes/process_database.py b/data/kbes/process_database.py
index a348fce7..fa4ac9e3 100644
--- a/data/kbes/process_database.py
+++ b/data/kbes/process_database.py
@@ -1,11 +1,13 @@
 # process_database.py for KBES dataset
 
-import pandas as pd
 import argparse
-from nkululeko.utils.files import find_files
 import os
+
+import pandas as pd
 from sklearn.model_selection import train_test_split
 
+from nkululeko.utils.files import find_files
+
 
 def process_database(data_dir, output_dir):
     # check if data_dir exist
diff --git a/data/kia/process_database.py b/data/kia/process_database.py
index 315c19d6..4b8913eb 100644
--- a/data/kia/process_database.py
+++ b/data/kia/process_database.py
@@ -5,6 +5,7 @@
 
 import pandas as pd
 from sklearn.model_selection import train_test_split
+
 from nkululeko.utils.files import find_files
 
 
diff --git a/data/make_readme.py b/data/make_readme.py
index a5aede7b..4815093f 100644
--- a/data/make_readme.py
+++ b/data/make_readme.py
@@ -1,8 +1,7 @@
 import os
 
-from mdutils import MdUtils
 import yaml
-
+from mdutils import MdUtils
 
 # root = "./data"
 root = "."
@@ -11,9 +10,9 @@
 mdFile.create_md_file()
 mdFile.new_header(level=1, title="Data")
 mdFile.new_paragraph(
-    "This is the default top directory for Nkululeko data import."
+    "This is the default top directory for Nkululeko data import. "
     "Each database should be in its own subfolder "
-    "(you can also use `ln -sf`` to soft link original database "
+    "(you can also use `ln -sf` to soft link original database "
     "path to these subfolders) "
     "and contain a README how to import the data to Nkululeko CSV or audformat."
 )
@@ -34,37 +33,34 @@
 )
 mdFile.new_paragraph(
     "To support open science and reproducible research, "
-    "we only accept PR and recipes for public dataset for "
+    "we encourage to submit PR and recipes for public dataset for "
     "now on."
 )
 
-mdFile.new_header(level=2, title="Databases")
-table_list = ["Name", "Target", "Description", "Access"]
+# read all lines in yaml file
+with open(os.path.join(root, "descr.yml")) as stream:
+    descr = yaml.safe_load(stream)
 
-db_folders = os.scandir(root)
+table_list = ["Name", "Target", "Description", "Access"]
 db_num = 0
-for f in db_folders:
-    if f.is_file():
-        continue
-    dir = f.name
-    descr = None
-    try:
-        with open(os.path.join(root, dir, "descr.yml")) as stream:
-            try:
-                descr = yaml.safe_load(stream)
-            except yaml.YAMLError as exc:
-                print(exc)
-    except FileNotFoundError as fnf:
-        pass
-    if descr != None:
-        table_list.extend(
-            [descr["name"], descr["target"], descr["descr"], descr["access"]]
-        )
-        db_num += 1
-    else:
-        print(f"error on {dir}")
+
+for dataset_name in descr.keys():
+    # print(dataset_name)
+    table_list.extend(
+        [
+            descr[dataset_name][0]["name"],
+            descr[dataset_name][1]["target"],
+            descr[dataset_name][2]["descr"],
+            descr[dataset_name][3]["access"],
+        ]
+    )
+    db_num += 1
+
 mdFile.new_table(columns=4, rows=db_num + 1, text=table_list, text_align="left")
 
+# add information about number of datasets
+mdFile.new_paragraph(f"This recipe contains information about {db_num} datasets.")
+
 mdFile.new_header(level=2, title="Performance")
 mdFile.new_line(
     mdFile.new_inline_image(
diff --git a/data/mlendsnd/process_database.py b/data/mlendsnd/process_database.py
index 1a7bfb41..e758c436 100644
--- a/data/mlendsnd/process_database.py
+++ b/data/mlendsnd/process_database.py
@@ -5,6 +5,7 @@
 from pathlib import Path
 
 import pandas as pd
+
 from nkululeko.utils import files
 
 
diff --git a/data/msp-improv/process_database2.py b/data/msp-improv/process_database2.py
index b5c10817..18e13ba2 100644
--- a/data/msp-improv/process_database2.py
+++ b/data/msp-improv/process_database2.py
@@ -3,10 +3,11 @@
 # bagustris@yahoo.com, 2023-08-22
 
 
+import argparse
+import csv
 import os
+
 import pandas as pd
-import csv
-import argparse
 
 train_list = []
 dev_list = []
diff --git a/data/msp-podcast/process_database.py b/data/msp-podcast/process_database.py
index c7eb8428..d3bab5cf 100644
--- a/data/msp-podcast/process_database.py
+++ b/data/msp-podcast/process_database.py
@@ -1,8 +1,11 @@
+import argparse
+import glob
 import os
-from os.path import basename, splitext, join as path_join
+from os.path import basename
+from os.path import join as path_join
+from os.path import splitext
+
 import pandas as pd
-import glob
-import argparse
 
 LABEL_DIR_PATH = 'Labels'
 WAV_DIR_PATH = 'Audios'
diff --git a/data/nemo/process_database.py b/data/nemo/process_database.py
index 2cfdf9e9..619eba02 100644
--- a/data/nemo/process_database.py
+++ b/data/nemo/process_database.py
@@ -3,6 +3,7 @@
 
 # from nkululeko.utils import find_files
 import os
+
 import pandas as pd
 from sklearn.model_selection import train_test_split
 
diff --git a/data/odyssey-cat-2024/process_database.py b/data/odyssey-cat-2024/process_database.py
index bfb0f8a3..ffa35772 100644
--- a/data/odyssey-cat-2024/process_database.py
+++ b/data/odyssey-cat-2024/process_database.py
@@ -1,6 +1,7 @@
-import pandas as pd
 import argparse
 
+import pandas as pd
+
 # Use this code to create a .csv file with the necessary format needed for 
 # categorical emotion recognition model
 
diff --git a/data/oreau2/process_database.py b/data/oreau2/process_database.py
index 89a446c3..e5b2fef7 100644
--- a/data/oreau2/process_database.py
+++ b/data/oreau2/process_database.py
@@ -6,6 +6,7 @@
 from pathlib import Path
 
 import pandas as pd
+
 from nkululeko.utils import files
 
 emotion_map = {
diff --git a/data/polish/process_database.py b/data/polish/process_database.py
index 725311f5..0ef718f3 100644
--- a/data/polish/process_database.py
+++ b/data/polish/process_database.py
@@ -1,9 +1,9 @@
 # process_database.py for POLISH dataset
 
-from pathlib import Path
-import pandas as pd
 import argparse
+from pathlib import Path
 
+import pandas as pd
 
 # emotion mapping , a: anger, f: fear, n:n neutral
 emotion_mapping = {
diff --git a/data/ravdess/process_database.py b/data/ravdess/process_database.py
index c257f966..4a02e7b8 100644
--- a/data/ravdess/process_database.py
+++ b/data/ravdess/process_database.py
@@ -21,8 +21,10 @@
 
  
 import os
-import pandas as pd
 import sys
+
+import pandas as pd
+
 <<<<<<< HEAD
 
 # ravdess source directory as argument
diff --git a/data/svd/process_database.py b/data/svd/process_database.py
index 6596bf14..e99bff12 100644
--- a/data/svd/process_database.py
+++ b/data/svd/process_database.py
@@ -1,5 +1,6 @@
 import pandas as pd
 
+
 def process_database(output_path):
     # Read the list files
     train_normal = pd.read_csv('train_normal.lst', header=None)
diff --git a/data/turev/process_database.py b/data/turev/process_database.py
index 2bd203ea..927df5c3 100644
--- a/data/turev/process_database.py
+++ b/data/turev/process_database.py
@@ -2,9 +2,11 @@
 
 import argparse
 from pathlib import Path
-from nkululeko.utils.files import find_files
+
 import pandas as pd
 
+from nkululeko.utils.files import find_files
+
 
 def process_database(data_dir, output_dir):  
     # check if data_dir exist
diff --git a/ini_file.md b/ini_file.md
index f62435ab..c9c9a826 100644
--- a/ini_file.md
+++ b/ini_file.md
@@ -284,6 +284,8 @@
     * **tree_reg**: Classification tree regressor
     * **xgb**: XG-Boost
     * **xgr**: XG-Boost Regression
+* **balancing**: balancing for **finetune** type; for other than finetune, set balancing in [FEATS].
+  * possible values: [ros, smote, adasyn]
 * **tuning_params**: possible tuning parameters for x-fold optimization (for Bayes, KNN, KNN_reg, Tree, Tree_reg, SVM, SVR, XGB and XGR)
   * tuning_params = ['subsample', 'n_estimators', 'max_depth']
     * subsample = [.5, .7]
diff --git a/meta/demos/demo_best_model.py b/meta/demos/demo_best_model.py
index 8d8e91f9..d0c6d724 100644
--- a/meta/demos/demo_best_model.py
+++ b/meta/demos/demo_best_model.py
@@ -2,28 +2,30 @@
 # Demonstration code to use the ML-experiment framework
 
 import sys
+
 sys.path.append("./src")
-from reporter import Reporter
-import experiment as exp
 import configparser
+
+import experiment as exp
 from util import Util
 
+
 def main(config_file):
     # load one configuration per experiment
     config = configparser.ConfigParser()
     config.read(config_file)
     util = Util()
-    
+
     # create a new experiment
     expr = exp.Experiment(config)
-    print(f'running {expr.name}')
+    print(f"running {expr.name}")
 
     # load the experiment
-    expr.load(f'{util.get_exp_name()}.pkl')
+    expr.load(f"{util.get_exp_name()}.pkl")
 
     expr.demo()
 
-    print('DONE')
+    print("DONE")
 
 
 if __name__ == "__main__":
diff --git a/meta/demos/multiple_experiments/do_experiments.py b/meta/demos/multiple_experiments/do_experiments.py
index d75b7834..2f924700 100644
--- a/meta/demos/multiple_experiments/do_experiments.py
+++ b/meta/demos/multiple_experiments/do_experiments.py
@@ -1,6 +1,5 @@
 import os
 
-
 classifiers = [
     {"--model": "mlp", "--layers": "\"{'l1':64,'l2':16}\"", "--epochs": 100},
     {
diff --git a/meta/demos/my_experiment.py b/meta/demos/my_experiment.py
index 22e700b7..be6c3750 100644
--- a/meta/demos/my_experiment.py
+++ b/meta/demos/my_experiment.py
@@ -2,34 +2,37 @@
 # Demonstration code to use the ML-experiment framework
 
 import sys
+
 sys.path.append("./nkululeko/src")
-import experiment as exp
 import configparser
-from util import Util
 import os.path
+
 import constants
+import experiment as exp
+from util import Util
+
 
 def main(config_file):
     # test if the configuration file exists
     if not os.path.isfile(config_file):
-        print(f'ERROR: no such file: {config_file}')
+        print(f"ERROR: no such file: {config_file}")
         exit()
 
     # load one configuration per experiment
     config = configparser.ConfigParser()
     config.read(config_file)
-    
+
     # create a new experiment
     expr = exp.Experiment(config)
     util = Util()
-    util.debug(f'running {expr.name}, nkululeko version {constants.VERSION}')
+    util.debug(f"running {expr.name}, nkululeko version {constants.VERSION}")
 
     # load the data
     expr.load_datasets()
 
     # split into train and test
     expr.fill_train_and_tests()
-    util.debug(f'train shape : {expr.df_train.shape}, test shape:{expr.df_test.shape}')
+    util.debug(f"train shape : {expr.df_train.shape}, test shape:{expr.df_test.shape}")
 
     # extract features
     expr.extract_feats()
@@ -40,9 +43,10 @@ def main(config_file):
     # run the experiment
     expr.run()
 
-    print('DONE')
+    print("DONE")
+
 
 if __name__ == "__main__":
     cwd = os.path.dirname(os.path.abspath(__file__))
-    main('./nkululeko/demos/exp_emodb.ini')
+    main("./nkululeko/demos/exp_emodb.ini")
 #    main(sys.argv[1]) # use this if you want to state the config file path on command line
diff --git a/meta/demos/my_experiment_local.py b/meta/demos/my_experiment_local.py
index 81a739e7..df3a37c8 100644
--- a/meta/demos/my_experiment_local.py
+++ b/meta/demos/my_experiment_local.py
@@ -2,20 +2,23 @@
 # Demonstration code to use the ML-experiment framework
 
 import sys
+
 sys.path.append("./src")
-import experiment as exp
 import configparser
+
+import experiment as exp
 from util import Util
 
+
 def main(config_file):
     # load one configuration per experiment
     config = configparser.ConfigParser()
     config.read(config_file)
-    
+
     # create a new experiment
     expr = exp.Experiment(config)
     util = Util()
-    util.debug(f'running {expr.name}')
+    util.debug(f"running {expr.name}")
 
     # load the data
     expr.load_datasets()
@@ -32,13 +35,14 @@ def main(config_file):
     # run the experiment
     expr.run()
 
-    print('DONE')
+    print("DONE")
+
 
 if __name__ == "__main__":
-#    main('./demos/exp_danish_local.ini')
-#    main('./demos/exp_emodb_wav2vec.ini')
-#    main('./demos/exp_cross_wav2vec1pager.ini')
-#    main('./demos/exp_emodb_local.ini')
-    main('./demos/exp_cross_local.ini')
+    #    main('./demos/exp_danish_local.ini')
+    #    main('./demos/exp_emodb_wav2vec.ini')
+    #    main('./demos/exp_cross_wav2vec1pager.ini')
+    #    main('./demos/exp_emodb_local.ini')
+    main("./demos/exp_cross_local.ini")
 #    main('./demos/exp_bdtgfir pul_local.ini')
 #    main(sys.argv[1]) # use this if you want to state the config file path on command line
diff --git a/meta/demos/plot_faster_anim.py b/meta/demos/plot_faster_anim.py
index 575e65f0..e5744a2c 100644
--- a/meta/demos/plot_faster_anim.py
+++ b/meta/demos/plot_faster_anim.py
@@ -2,30 +2,33 @@
 # Demonstration code to use the ML-experiment framework
 
 import sys
+
 sys.path.append("./src")
-from reporter import Reporter
-import experiment as exp
 import configparser
+
+import experiment as exp
+from reporter import Reporter
 from util import Util
 
+
 def main(config_file):
     # load one configuration per experiment
     config = configparser.ConfigParser()
     config.read(config_file)
     util = Util()
-    
+
     # create a new experiment
     expr = exp.Experiment(config)
-    print(f'running {expr.name}')
+    print(f"running {expr.name}")
 
     # load the experiment
     # expr.load(f'{util.get_exp_name()}.pkl')
 
     reporter = Reporter([], [])
 
-    reporter.make_conf_animation('test.gif')
+    reporter.make_conf_animation("test.gif")
 
-    print('DONE')
+    print("DONE")
 
 
 if __name__ == "__main__":
diff --git a/nkululeko/aug_train.py b/nkululeko/aug_train.py
index 7318478d..4b42c95e 100644
--- a/nkululeko/aug_train.py
+++ b/nkululeko/aug_train.py
@@ -1,15 +1,17 @@
 # aug_train.py
 # train with augmentations
+import argparse
 import ast
+import configparser
 import os.path
+
 import numpy as np
-import configparser
-import argparse
+
 import nkululeko.experiment as exp
-from nkululeko.utils.util import Util
-from nkululeko.constants import VERSION
 import nkululeko.glob_conf as glob_conf
 from nkululeko.augment import doit as augment
+from nkululeko.constants import VERSION
+from nkululeko.utils.util import Util
 
 
 def doit(config_file):
diff --git a/nkululeko/augment.py b/nkululeko/augment.py
index 32ce249f..72f872cd 100644
--- a/nkululeko/augment.py
+++ b/nkululeko/augment.py
@@ -2,13 +2,15 @@
 # augment the training sets
 
 import argparse
-import pandas as pd
-import os
 import ast
-from nkululeko.experiment import Experiment
 import configparser
-from nkululeko.utils.util import Util
+import os
+
+import pandas as pd
+
 from nkululeko.constants import VERSION
+from nkululeko.experiment import Experiment
+from nkululeko.utils.util import Util
 
 
 def doit(config_file):
diff --git a/nkululeko/augmenting/augmenter.py b/nkululeko/augmenting/augmenter.py
index 47704967..5ecb480d 100644
--- a/nkululeko/augmenting/augmenter.py
+++ b/nkululeko/augmenting/augmenter.py
@@ -1,12 +1,12 @@
 # augmenter.py
 import os
-import numpy as np
+
+import audeer
+import audiofile
 import pandas as pd
 from audiomentations import *
 from tqdm import tqdm
-import audeer
-import audiofile
-from audformat.utils import map_file_path
+
 from nkululeko.utils.util import Util
 
 
diff --git a/nkululeko/augmenting/randomsplicer.py b/nkululeko/augmenting/randomsplicer.py
index 8d7584d2..b52b19b1 100644
--- a/nkululeko/augmenting/randomsplicer.py
+++ b/nkululeko/augmenting/randomsplicer.py
@@ -13,15 +13,15 @@
 
 """
 
-import pandas as pd
-from tqdm import tqdm
 import os
-import numpy as np
-import audiofile as af
-from audformat.utils import map_file_path
+
 import audeer
-from nkululeko.utils.util import Util
+import audiofile as af
+import pandas as pd
+from tqdm import tqdm
+
 import nkululeko.augmenting.randomsplicing as rsp
+from nkululeko.utils.util import Util
 
 
 class Randomsplicer:
diff --git a/nkululeko/augmenting/randomsplicing.py b/nkululeko/augmenting/randomsplicing.py
index 75bd0f70..cd0a7112 100644
--- a/nkululeko/augmenting/randomsplicing.py
+++ b/nkululeko/augmenting/randomsplicing.py
@@ -10,9 +10,9 @@
 F. Burkhardt, Anna Derington, Matthias Kahlau, Klaus Scherer, Florian Eyben and Björn Schuller: Masking Speech Contents by Random Splicing: is Emotional Expression Preserved?, Proc. ICASSP, 2023
 
 """
-import numpy as np
+
 import librosa
-import audiofile as af
+import numpy as np
 
 
 def random_splicing(
@@ -54,7 +54,6 @@ def split_wav_naive(wav, top_db=12):
 
 
 def remix_random_reverse(wav, indices, p_reverse=0):
-    import random
 
     wav_remix = []
 
diff --git a/nkululeko/augmenting/resampler.py b/nkululeko/augmenting/resampler.py
index 56ff60f2..58d8d340 100644
--- a/nkululeko/augmenting/resampler.py
+++ b/nkululeko/augmenting/resampler.py
@@ -2,12 +2,14 @@
 resample a data frame
 
 """
+
 import os
 import shutil
 
 import audformat
 import pandas as pd
 import torchaudio
+
 from nkululeko.utils.util import Util
 
 
@@ -18,8 +20,11 @@ def __init__(self, df, replace, not_testing=True):
         self.util = Util("resampler", has_config=not_testing)
         self.util.warn(f"all files might be resampled to {self.SAMPLING_RATE}")
         self.not_testing = not_testing
-        self.replace = eval(self.util.config_val(
-            "RESAMPLE", "replace", "False")) if not not_testing else replace
+        self.replace = (
+            eval(self.util.config_val("RESAMPLE", "replace", "False"))
+            if not not_testing
+            else replace
+        )
 
     def resample(self):
         files = self.df.index.get_level_values(0).values
@@ -45,8 +50,7 @@ def resample(self):
                 continue
             if org_sr != self.SAMPLING_RATE:
                 self.util.debug(f"resampling {f} (sr = {org_sr})")
-                resampler = torchaudio.transforms.Resample(
-                    org_sr, self.SAMPLING_RATE)
+                resampler = torchaudio.transforms.Resample(org_sr, self.SAMPLING_RATE)
                 signal = resampler(signal)
                 if replace:
                     torchaudio.save(
@@ -63,8 +67,7 @@ def resample(self):
             self.df = self.df.set_index(
                 self.df.index.set_levels(new_files, level="file")
             )
-            target_file = self.util.config_val(
-                "RESAMPLE", "target", "resampled.csv")
+            target_file = self.util.config_val("RESAMPLE", "target", "resampled.csv")
             # remove encoded labels
             target = self.util.config_val("DATA", "target", "emotion")
             if "class_label" in self.df.columns:
diff --git a/nkululeko/autopredict/ap_age.py b/nkululeko/autopredict/ap_age.py
index d407cfdf..a92743f7 100644
--- a/nkululeko/autopredict/ap_age.py
+++ b/nkululeko/autopredict/ap_age.py
@@ -2,10 +2,12 @@
 A predictor for age.
 Currently based on audEERING's agender model.
 """
-from nkululeko.utils.util import Util
-from nkululeko.feature_extractor import FeatureExtractor
+
 import ast
+
 import nkululeko.glob_conf as glob_conf
+from nkululeko.feature_extractor import FeatureExtractor
+from nkululeko.utils.util import Util
 
 
 class AgePredictor:
diff --git a/nkululeko/autopredict/ap_arousal.py b/nkululeko/autopredict/ap_arousal.py
index 8dab9f95..c61b4440 100644
--- a/nkululeko/autopredict/ap_arousal.py
+++ b/nkululeko/autopredict/ap_arousal.py
@@ -2,10 +2,12 @@
 A predictor for emotional arousal.
 Currently based on audEERING's emotional dimension model.
 """
-from nkululeko.utils.util import Util
-from nkululeko.feature_extractor import FeatureExtractor
+
 import ast
+
 import nkululeko.glob_conf as glob_conf
+from nkululeko.feature_extractor import FeatureExtractor
+from nkululeko.utils.util import Util
 
 
 class ArousalPredictor:
diff --git a/nkululeko/autopredict/ap_dominance.py b/nkululeko/autopredict/ap_dominance.py
index ad4babae..62c66d89 100644
--- a/nkululeko/autopredict/ap_dominance.py
+++ b/nkululeko/autopredict/ap_dominance.py
@@ -3,10 +3,11 @@
 Currently based on audEERING's emotional dimension model.
 """
 
-from nkululeko.utils.util import Util
-from nkululeko.feature_extractor import FeatureExtractor
 import ast
+
 import nkululeko.glob_conf as glob_conf
+from nkululeko.feature_extractor import FeatureExtractor
+from nkululeko.utils.util import Util
 
 
 class DominancePredictor:
diff --git a/nkululeko/autopredict/ap_gender.py b/nkululeko/autopredict/ap_gender.py
index ee32b6c0..04916f8e 100644
--- a/nkululeko/autopredict/ap_gender.py
+++ b/nkululeko/autopredict/ap_gender.py
@@ -2,10 +2,12 @@
 A predictor for biological sex.
 Currently based on audEERING's agender model.
 """
-from nkululeko.utils.util import Util
-from nkululeko.feature_extractor import FeatureExtractor
+
 import ast
+
 import nkululeko.glob_conf as glob_conf
+from nkululeko.feature_extractor import FeatureExtractor
+from nkululeko.utils.util import Util
 
 
 class GenderPredictor:
diff --git a/nkululeko/autopredict/ap_mos.py b/nkululeko/autopredict/ap_mos.py
index 701bc039..f74e3485 100644
--- a/nkululeko/autopredict/ap_mos.py
+++ b/nkululeko/autopredict/ap_mos.py
@@ -1,11 +1,14 @@
 """"
 A predictor for MOS - mean opinion score.
 """
-from nkululeko.utils.util import Util
+
 import ast
+
+import numpy as np
+
 import nkululeko.glob_conf as glob_conf
 from nkululeko.feature_extractor import FeatureExtractor
-import numpy as np
+from nkululeko.utils.util import Util
 
 
 class MOSPredictor:
diff --git a/nkululeko/autopredict/ap_pesq.py b/nkululeko/autopredict/ap_pesq.py
index 72e939ed..6bddd7ba 100644
--- a/nkululeko/autopredict/ap_pesq.py
+++ b/nkululeko/autopredict/ap_pesq.py
@@ -1,11 +1,14 @@
 """"
 A predictor for PESQ - Perceptual Evaluation of Speech Quality.
 """
-from nkululeko.utils.util import Util
+
 import ast
+
+import numpy as np
+
 import nkululeko.glob_conf as glob_conf
 from nkululeko.feature_extractor import FeatureExtractor
-import numpy as np
+from nkululeko.utils.util import Util
 
 
 class PESQPredictor:
diff --git a/nkululeko/autopredict/ap_sdr.py b/nkululeko/autopredict/ap_sdr.py
index 1e4c983e..88a9f161 100644
--- a/nkululeko/autopredict/ap_sdr.py
+++ b/nkululeko/autopredict/ap_sdr.py
@@ -2,11 +2,14 @@
 A predictor for SDR - Signal to Distortion Ratio.
 as estimated by Scale-Invariant Signal-to-Distortion Ratio (SI-SDR) 
 """
-from nkululeko.utils.util import Util
+
 import ast
+
+import numpy as np
+
 import nkululeko.glob_conf as glob_conf
 from nkululeko.feature_extractor import FeatureExtractor
-import numpy as np
+from nkululeko.utils.util import Util
 
 
 class SDRPredictor:
diff --git a/nkululeko/autopredict/ap_snr.py b/nkululeko/autopredict/ap_snr.py
index 9fc0dab5..35dcd0a7 100644
--- a/nkululeko/autopredict/ap_snr.py
+++ b/nkululeko/autopredict/ap_snr.py
@@ -1,11 +1,14 @@
 """"
 A predictor for SNR - signal-to-noise ratio.
 """
-from nkululeko.utils.util import Util
+
 import ast
+
+import numpy as np
+
 import nkululeko.glob_conf as glob_conf
 from nkululeko.feature_extractor import FeatureExtractor
-import numpy as np
+from nkululeko.utils.util import Util
 
 
 class SNRPredictor:
diff --git a/nkululeko/autopredict/ap_stoi.py b/nkululeko/autopredict/ap_stoi.py
index 3ca40b2c..bfc972d4 100644
--- a/nkululeko/autopredict/ap_stoi.py
+++ b/nkululeko/autopredict/ap_stoi.py
@@ -1,11 +1,14 @@
 """"
 A predictor for STOI - Short-Time Objective Intelligibility (STOI)
 """
-from nkululeko.utils.util import Util
+
 import ast
+
+import numpy as np
+
 import nkululeko.glob_conf as glob_conf
 from nkululeko.feature_extractor import FeatureExtractor
-import numpy as np
+from nkululeko.utils.util import Util
 
 
 class STOIPredictor:
diff --git a/nkululeko/autopredict/ap_valence.py b/nkululeko/autopredict/ap_valence.py
index edbc96c9..2349d2e5 100644
--- a/nkululeko/autopredict/ap_valence.py
+++ b/nkululeko/autopredict/ap_valence.py
@@ -2,10 +2,12 @@
 A predictor for emotional valence.
 Currently based on audEERING's emotional dimension model.
 """
-from nkululeko.utils.util import Util
-from nkululeko.feature_extractor import FeatureExtractor
+
 import ast
+
 import nkululeko.glob_conf as glob_conf
+from nkululeko.feature_extractor import FeatureExtractor
+from nkululeko.utils.util import Util
 
 
 class ValencePredictor:
diff --git a/nkululeko/autopredict/estimate_snr.py b/nkululeko/autopredict/estimate_snr.py
index b75135b6..da4aa948 100644
--- a/nkululeko/autopredict/estimate_snr.py
+++ b/nkululeko/autopredict/estimate_snr.py
@@ -43,9 +43,7 @@ def __init__(self, input_data, sample_rate, window_size=320, hop_size=160):
     def frame_audio(self, signal):
         num_frames = 1 + (len(signal) - self.frame_length) // self.hop_length
         frames = [
-            signal[
-                i * self.hop_length: (i * self.hop_length) + self.frame_length
-            ]
+            signal[i * self.hop_length : (i * self.hop_length) + self.frame_length]
             for i in range(num_frames)
         ]
         return frames
@@ -64,11 +62,8 @@ def estimate_snr(self):
             for frame in frames
         ]
 
-        energy_threshold_low = np.percentile(
-            log_energies, 25)  # First quartile
-        energy_threshold_high = np.percentile(
-            log_energies, 75
-        )  # Third quartile
+        energy_threshold_low = np.percentile(log_energies, 25)  # First quartile
+        energy_threshold_high = np.percentile(log_energies, 75)  # Third quartile
 
         low_energy_frames = [
             log_energy
@@ -94,9 +89,7 @@ def estimate_snr(self):
             energy_threshold_high,
         )
 
-    def plot_energy(
-        self, log_energies, energy_threshold_low, energy_threshold_high
-    ):
+    def plot_energy(self, log_energies, energy_threshold_low, energy_threshold_high):
         plt.figure(figsize=(10, 6))
         plt.plot(log_energies, label="Log Energy")
         plt.axhline(
@@ -153,9 +146,12 @@ def main():
 
     signal, sr = audiofile.read(args.input)
     snr_estimator = SNREstimator(signal, sr, args.window_size, args.hop_size)
-    estimated_snr, log_energies, energy_threshold_low, energy_threshold_high = (
-        snr_estimator.estimate_snr()
-    )
+    (
+        estimated_snr,
+        log_energies,
+        energy_threshold_low,
+        energy_threshold_high,
+    ) = snr_estimator.estimate_snr()
 
     print("Estimated SNR:", estimated_snr)
 
diff --git a/nkululeko/cacheddataset.py b/nkululeko/cacheddataset.py
index 93f29ca2..237b337b 100644
--- a/nkululeko/cacheddataset.py
+++ b/nkululeko/cacheddataset.py
@@ -1,5 +1,5 @@
-import torch
 import numpy as np
+import torch
 
 
 class CachedDataset(torch.utils.data.Dataset):
diff --git a/nkululeko/constants.py b/nkululeko/constants.py
index 9cb657f5..47579caf 100644
--- a/nkululeko/constants.py
+++ b/nkululeko/constants.py
@@ -1,2 +1,2 @@
-VERSION="0.90.0"
+VERSION = "0.90.0"
 SAMPLING_RATE = 16000
diff --git a/nkululeko/data/dataset.py b/nkululeko/data/dataset.py
index d89ffc85..326b7e7f 100644
--- a/nkululeko/data/dataset.py
+++ b/nkululeko/data/dataset.py
@@ -3,13 +3,11 @@
 import os
 import os.path
 from random import sample
-import numpy as np
-import pandas as pd
 
 import audformat
-from audformat.utils import duration
+import numpy as np
+import pandas as pd
 
-import nkululeko.filter_data as filter
 import nkululeko.glob_conf as glob_conf
 from nkululeko.filter_data import DataFilter
 from nkululeko.plots import Plots
@@ -274,20 +272,20 @@ def _get_df_for_lists(self, db, df_files):
                 # try to get the target values
                 df_local[self.target] = source_df[self.col_label]
                 is_labeled = True
-            except (KeyError, ValueError, audformat.errors.BadKeyError) as e:
+            except (KeyError, ValueError, audformat.errors.BadKeyError):
                 pass
             try:
                 # try to get the speaker values
                 df_local["speaker"] = source_df["speaker"]
                 got_speaker = True
-            except (KeyError, ValueError, audformat.errors.BadKeyError) as e:
+            except (KeyError, ValueError, audformat.errors.BadKeyError):
                 pass
             try:
                 # try to get the gender values
                 if "gender" in source_df:
                     df_local["gender"] = source_df["gender"]
                     got_gender = True
-            except (KeyError, ValueError, audformat.errors.BadKeyError) as e:
+            except (KeyError, ValueError, audformat.errors.BadKeyError):
                 pass
             try:
                 # try to get the age values
@@ -311,13 +309,13 @@ def _get_df_for_lists(self, db, df_files):
                 # also it might be possible that the age is part of the speaker description
                 df_local["age"] = db[table]["speaker"].get(map="age").astype(int)
                 got_age = True
-            except (ValueError, audformat.errors.BadKeyError) as e:
+            except (ValueError, audformat.errors.BadKeyError):
                 pass
             try:
                 # same for the target, e.g. "age"
                 df_local[self.target] = db[table]["speaker"].get(map=self.target)
                 is_labeled = True
-            except (ValueError, audformat.core.errors.BadKeyError) as e:
+            except (ValueError, audformat.core.errors.BadKeyError):
                 pass
             # copy other column
             for column in source_df.columns:
@@ -463,8 +461,7 @@ def finish_up(self, df, storage):
 
     def balanced_split(self):
         """One way to split train and eval sets: Generate split dataframes for some balancing criterion"""
-        from splitutils import binning
-        from splitutils import optimize_traintest_split
+        from splitutils import binning, optimize_traintest_split
 
         seed = 42
         k = 30
@@ -473,7 +470,7 @@ def balanced_split(self):
         # split target
         targets = df[self.target].to_numpy()
         #
-        bins = self.util.config_val("DATA", f"bin", False)
+        bins = self.util.config_val("DATA", "bin", False)
         if bins:
             nbins = len(ast.literal_eval(bins))
             targets = binning(targets, nbins=nbins)
@@ -481,7 +478,7 @@ def balanced_split(self):
         speakers = df["speaker"].to_numpy()
 
         # on which variables (targets, groupings) to stratify
-        stratif_vars = self.util.config_val("DATA", f"balance", False)
+        stratif_vars = self.util.config_val("DATA", "balance", False)
         stratif_vars_array = {}
         if not stratif_vars:
             self.util.error("balanced split needs stratif_vars to stratify the splits")
@@ -500,7 +497,7 @@ def balanced_split(self):
         # weights for all stratify_on variables and
         # and for test proportion match. Give target
         # variable EMOTION more weight than groupings.
-        size_diff = int(self.util.config_val("DATA", f"size_diff_weight", "1"))
+        size_diff = int(self.util.config_val("DATA", "size_diff_weight", "1"))
         weights = {
             "size_diff": size_diff,
         }
diff --git a/nkululeko/data/dataset_csv.py b/nkululeko/data/dataset_csv.py
index 2c279e92..a1cb6e92 100644
--- a/nkululeko/data/dataset_csv.py
+++ b/nkululeko/data/dataset_csv.py
@@ -2,10 +2,12 @@
 import ast
 import os
 import os.path
-import pandas as pd
+
 import audformat.utils
-from nkululeko.data.dataset import Dataset
+import pandas as pd
+
 import nkululeko.glob_conf as glob_conf
+from nkululeko.data.dataset import Dataset
 from nkululeko.reporting.report_item import ReportItem
 
 
@@ -81,7 +83,7 @@ def load(self):
         self.start_fresh = eval(self.util.config_val("DATA", "no_reuse", "False"))
         is_index = False
         try:
-            if self.is_labeled and not "class_label" in self.df.columns:
+            if self.is_labeled and "class_label" not in self.df.columns:
                 self.df["class_label"] = self.df[self.target]
         except AttributeError:
             is_index = True
diff --git a/nkululeko/demo-ft.py b/nkululeko/demo-ft.py
new file mode 100644
index 00000000..752a05ed
--- /dev/null
+++ b/nkululeko/demo-ft.py
@@ -0,0 +1,29 @@
+import argparse
+import configparser
+import os
+
+from sklearn import pipeline
+from transformers import pipelines
+
+from nkululeko.utils.util import get_exp_dir
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--file", help="A file that should be processed (16kHz mono wav)")
+# read config from ini file
+parser.add_argument("--config", default="exp.ini", help="The base configuration")
+
+
+args = parser.parse_args()
+file = args.file
+config = configparser.ConfigParser()
+
+# get exp dir from config [EXP][root][name] + models + run_0 + torch
+config.read(args.config)
+exp_dir = get_exp_dir("model_path")
+
+# exp_dir = get_exp_dir("model_path")
+model_path = os.path.join(exp_dir, "model")
+pipe = pipelines("audio-classification", model=model_path)
+
+
+print(pipeline(file))
diff --git a/nkululeko/demo_feats.py b/nkululeko/demo_feats.py
index d5fc822d..2431b9f9 100644
--- a/nkululeko/demo_feats.py
+++ b/nkululeko/demo_feats.py
@@ -1,13 +1,14 @@
 # demo_feats.py
 # Test some features extracted
 
-from nkululeko.experiment import Experiment
-import configparser
-from nkululeko.utils.util import Util
-from nkululeko.constants import VERSION
 import argparse
+import configparser
 import os
 
+from nkululeko.constants import VERSION
+from nkululeko.experiment import Experiment
+from nkululeko.utils.util import Util
+
 
 def main(src_dir):
     parser = argparse.ArgumentParser(description="Call the nkululeko framework.")
diff --git a/nkululeko/demo_predictor.py b/nkululeko/demo_predictor.py
index 006bbd9b..634b8d14 100644
--- a/nkululeko/demo_predictor.py
+++ b/nkululeko/demo_predictor.py
@@ -1,11 +1,10 @@
 # demo_predictor.py
 import os
 
-import numpy as np
-import pandas as pd
-
 import audformat
 import audiofile
+import numpy as np
+import pandas as pd
 
 import nkululeko.glob_conf as glob_conf
 from nkululeko.utils.util import Util
@@ -46,7 +45,7 @@ def run_demo(self):
                         )
                     else:
                         file_list = in_df.index.values
-                except (ValueError, AttributeError) as error:
+                except (ValueError, AttributeError):
                     with open(self.file) as f:
                         first = True
                         for index, line in enumerate(f):
diff --git a/nkululeko/ensemble.py b/nkululeko/ensemble.py
index b467cf2a..c0fe8fdb 100644
--- a/nkululeko/ensemble.py
+++ b/nkululeko/ensemble.py
@@ -15,28 +15,20 @@
     ValueError: If an unknown ensemble method is provided.
     AssertionError: If the number of config files is less than 2 for majority voting.
 """
+
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
 
-from typing import List
 import configparser
 import time
 from argparse import ArgumentParser
 from pathlib import Path
+from typing import List
 
 import numpy as np
 import pandas as pd
-import matplotlib.pyplot as plt
-
-from sklearn.metrics import(
-    RocCurveDisplay,
-    balanced_accuracy_score, 
-    classification_report,
-    auc,
-    roc_auc_score,
-    roc_curve
-)
+from sklearn.metrics import balanced_accuracy_score, classification_report
 
 from nkululeko.constants import VERSION
 from nkululeko.experiment import Experiment
@@ -169,17 +161,19 @@ def performance_weighted_ensemble(ensemble_preds_ls, labels, weights):
 
     # asserts weiths in decimal 0-1
     assert all(0 <= w <= 1 for w in weights), "Weights must be between 0 and 1"
-    
+
     # assert lenght of weights matches number of models
-    assert len(weights) == len(ensemble_preds_ls), "Number of weights must match number of models"
-    
+    assert len(weights) == len(
+        ensemble_preds_ls
+    ), "Number of weights must match number of models"
+
     # Normalize weights
     total_weight = sum(weights)
     weights = [weight / total_weight for weight in weights]
-    
+
     for idx in ensemble_preds_ls[0].index:
         class_probabilities = {label: 0 for label in labels}
-        
+
         for df, weight in zip(ensemble_preds_ls, weights):
             row = df.loc[idx]
             for label in labels:
@@ -192,10 +186,12 @@ def performance_weighted_ensemble(ensemble_preds_ls, labels, weights):
     return final_predictions, final_confidences
 
 
-
-
 def ensemble_predictions(
-    config_files: List[str], method: str, threshold: float, weights: List[float], no_labels: bool
+    config_files: List[str],
+    method: str,
+    threshold: float,
+    weights: List[float],
+    no_labels: bool,
 ) -> pd.DataFrame:
     """
     Ensemble predictions from multiple experiments.
@@ -261,17 +257,20 @@ def ensemble_predictions(
             ensemble_preds_ls, labels, threshold
         )
     elif method == "uncertainty_weighted":
-        ensemble_preds["predicted"], ensemble_preds["uncertainty"] = (
-            uncertainty_weighted_ensemble(ensemble_preds_ls, labels)
-        )
+        (
+            ensemble_preds["predicted"],
+            ensemble_preds["uncertainty"],
+        ) = uncertainty_weighted_ensemble(ensemble_preds_ls, labels)
     elif method == "confidence_weighted":
-        ensemble_preds["predicted"], ensemble_preds["confidence"] = (
-            confidence_weighted_ensemble(ensemble_preds_ls, labels)
-        )
+        (
+            ensemble_preds["predicted"],
+            ensemble_preds["confidence"],
+        ) = confidence_weighted_ensemble(ensemble_preds_ls, labels)
     elif method == "performance_weighted":
-        ensemble_preds["predicted"], ensemble_preds["confidence"] = (
-            performance_weighted_ensemble(ensemble_preds_ls, labels, weights)
-        )
+        (
+            ensemble_preds["predicted"],
+            ensemble_preds["confidence"],
+        ) = performance_weighted_ensemble(ensemble_preds_ls, labels, weights)
     else:
         raise ValueError(f"Unknown ensemble method: {method}")
 
diff --git a/nkululeko/experiment.py b/nkululeko/experiment.py
index d07b0687..6c6c60a9 100644
--- a/nkululeko/experiment.py
+++ b/nkululeko/experiment.py
@@ -5,13 +5,13 @@
 import random
 import time
 
+import audeer
+import audformat
 import numpy as np
 import pandas as pd
 from sklearn.preprocessing import LabelEncoder
 
-import audeer
-import audformat
-
+import nkululeko.glob_conf as glob_conf
 from nkululeko.data.dataset import Dataset
 from nkululeko.data.dataset_csv import Dataset_CSV
 from nkululeko.demo_predictor import Demo_predictor
@@ -19,7 +19,6 @@
 from nkululeko.feature_extractor import FeatureExtractor
 from nkululeko.file_checker import FileChecker
 from nkululeko.filter_data import DataFilter
-import nkululeko.glob_conf as glob_conf
 from nkululeko.plots import Plots
 from nkululeko.reporting.report import Report
 from nkululeko.runmanager import Runmanager
@@ -744,7 +743,6 @@ def save_onnx(self, filename):
         if model.is_ann():
             print("converting to onnx from torch")
         else:
-
             print("converting to onnx from sklearn")
         # save the rest
         f = open(filename, "wb")
diff --git a/nkululeko/experiment_felix.py b/nkululeko/experiment_felix.py
new file mode 100644
index 00000000..b7830387
--- /dev/null
+++ b/nkululeko/experiment_felix.py
@@ -0,0 +1,728 @@
+# experiment.py: Main class for an experiment (nkululeko.nkululeko)
+import ast
+import os
+import pickle
+import random
+import time
+
+import audeer
+import audformat
+import numpy as np
+import pandas as pd
+from sklearn.preprocessing import LabelEncoder
+
+import nkululeko.glob_conf as glob_conf
+from nkululeko.data.dataset import Dataset
+from nkululeko.data.dataset_csv import Dataset_CSV
+from nkululeko.demo_predictor import Demo_predictor
+from nkululeko.feat_extract.feats_analyser import FeatureAnalyser
+from nkululeko.feature_extractor import FeatureExtractor
+from nkululeko.file_checker import FileChecker
+from nkululeko.filter_data import DataFilter
+from nkululeko.plots import Plots
+from nkululeko.reporting.report import Report
+from nkululeko.runmanager import Runmanager
+from nkululeko.scaler import Scaler
+from nkululeko.test_predictor import TestPredictor
+from nkululeko.utils.util import Util
+
+
+class Experiment:
+    """Main class specifying an experiment"""
+
+    def __init__(self, config_obj):
+        """
+        Parameters
+        ----------
+        config_obj : a config parser object that sets the experiment parameters and being set as a global object.
+        """
+
+        self.set_globals(config_obj)
+        self.name = glob_conf.config["EXP"]["name"]
+        self.root = os.path.join(glob_conf.config["EXP"]["root"], "")
+        self.data_dir = os.path.join(self.root, self.name)
+        audeer.mkdir(self.data_dir)  # create the experiment directory
+        self.util = Util("experiment")
+        glob_conf.set_util(self.util)
+        fresh_report = eval(self.util.config_val("REPORT", "fresh", "False"))
+        if not fresh_report:
+            try:
+                with open(os.path.join(self.data_dir, "report.pkl"), "rb") as handle:
+                    self.report = pickle.load(handle)
+            except FileNotFoundError:
+                self.report = Report()
+        else:
+            self.util.debug("starting a fresh report")
+            self.report = Report()
+        glob_conf.set_report(self.report)
+        self.loso = self.util.config_val("MODEL", "loso", False)
+        self.logo = self.util.config_val("MODEL", "logo", False)
+        self.xfoldx = self.util.config_val("MODEL", "k_fold_cross", False)
+        self.start = time.process_time()
+
+    def set_module(self, module):
+        glob_conf.set_module(module)
+
+    def store_report(self):
+        with open(os.path.join(self.data_dir, "report.pkl"), "wb") as handle:
+            pickle.dump(self.report, handle)
+        if eval(self.util.config_val("REPORT", "show", "False")):
+            self.report.print()
+        if self.util.config_val("REPORT", "latex", False):
+            self.report.export_latex()
+
+    def get_name(self):
+        return self.util.get_exp_name()
+
+    def set_globals(self, config_obj):
+        """install a config object in the global space"""
+        glob_conf.init_config(config_obj)
+
+    def load_datasets(self):
+        """Load all databases specified in the configuration and map the labels"""
+        ds = ast.literal_eval(glob_conf.config["DATA"]["databases"])
+        self.datasets = {}
+        self.got_speaker, self.got_gender, self.got_age = False, False, False
+        for d in ds:
+            ds_type = self.util.config_val_data(d, "type", "audformat")
+            if ds_type == "audformat":
+                data = Dataset(d)
+            elif ds_type == "csv":
+                data = Dataset_CSV(d)
+            else:
+                self.util.error(f"unknown data type: {ds_type}")
+            data.load()
+            data.prepare()
+            if data.got_gender:
+                self.got_gender = True
+            if data.got_age:
+                self.got_age = True
+            if data.got_speaker:
+                self.got_speaker = True
+            self.datasets.update({d: data})
+        self.target = self.util.config_val("DATA", "target", "emotion")
+        glob_conf.set_target(self.target)
+        # print target via debug
+        self.util.debug(f"target: {self.target}")
+        # print keys/column
+        dbs = ",".join(list(self.datasets.keys()))
+        labels = self.util.config_val("DATA", "labels", False)
+        if labels:
+            self.labels = ast.literal_eval(labels)
+            self.util.debug(f"Target labels (from config): {labels}")
+        else:
+            self.labels = list(
+                next(iter(self.datasets.values())).df[self.target].unique()
+            )
+            self.util.debug(f"Target labels (from database): {labels}")
+        glob_conf.set_labels(self.labels)
+        self.util.debug(f"loaded databases {dbs}")
+
+    def _import_csv(self, storage):
+        # df = pd.read_csv(storage, header=0, index_col=[0,1,2])
+        # df.index.set_levels(pd.to_timedelta(df.index.levels[1]), level=1)
+        # df.index.set_levels(pd.to_timedelta(df.index.levels[2]), level=2)
+        df = audformat.utils.read_csv(storage)
+        df.is_labeled = True if self.target in df else False
+        # print(df.head())
+        return df
+
+    def fill_tests(self):
+        """Only fill a new test set"""
+
+        test_dbs = ast.literal_eval(glob_conf.config["DATA"]["tests"])
+        self.df_test = pd.DataFrame()
+        start_fresh = eval(self.util.config_val("DATA", "no_reuse", "False"))
+        store = self.util.get_path("store")
+        storage_test = f"{store}extra_testdf.csv"
+        if os.path.isfile(storage_test) and not start_fresh:
+            self.util.debug(f"reusing previously stored {storage_test}")
+            self.df_test = self._import_csv(storage_test)
+        else:
+            for d in test_dbs:
+                ds_type = self.util.config_val_data(d, "type", "audformat")
+                if ds_type == "audformat":
+                    data = Dataset(d)
+                elif ds_type == "csv":
+                    data = Dataset_CSV(d)
+                else:
+                    self.util.error(f"unknown data type: {ds_type}")
+                data.load()
+                if data.got_gender:
+                    self.got_gender = True
+                if data.got_age:
+                    self.got_age = True
+                if data.got_speaker:
+                    self.got_speaker = True
+                data.split()
+                data.prepare_labels()
+                self.df_test = pd.concat(
+                    [self.df_test, self.util.make_segmented_index(data.df_test)]
+                )
+                self.df_test.is_labeled = data.is_labeled
+            self.df_test.got_gender = self.got_gender
+            self.df_test.got_speaker = self.got_speaker
+            # self.util.set_config_val('FEATS', 'needs_features_extraction', 'True')
+            # self.util.set_config_val('FEATS', 'no_reuse', 'True')
+            self.df_test["class_label"] = self.df_test[self.target]
+            self.df_test[self.target] = self.label_encoder.transform(
+                self.df_test[self.target]
+            )
+            self.df_test.to_csv(storage_test)
+
+    def fill_train_and_tests(self):
+        """Set up train and development sets. The method should be specified in the config."""
+        store = self.util.get_path("store")
+        storage_test = f"{store}testdf.csv"
+        storage_train = f"{store}traindf.csv"
+        start_fresh = eval(self.util.config_val("DATA", "no_reuse", "False"))
+        if (
+            os.path.isfile(storage_train)
+            and os.path.isfile(storage_test)
+            and not start_fresh
+        ):
+            self.util.debug(
+                f"reusing previously stored {storage_test} and {storage_train}"
+            )
+            self.df_test = self._import_csv(storage_test)
+            # print(f"df_test: {self.df_test}")
+            self.df_train = self._import_csv(storage_train)
+            # print(f"df_train: {self.df_train}")
+        else:
+            self.df_train, self.df_test = pd.DataFrame(), pd.DataFrame()
+            for d in self.datasets.values():
+                d.split()
+                d.prepare_labels()
+                if d.df_train.shape[0] == 0:
+                    self.util.debug(f"warn: {d.name} train empty")
+                self.df_train = pd.concat([self.df_train, d.df_train])
+                # print(f"df_train: {self.df_train}")
+                self.util.copy_flags(d, self.df_train)
+                if d.df_test.shape[0] == 0:
+                    self.util.debug(f"warn: {d.name} test empty")
+                self.df_test = pd.concat([self.df_test, d.df_test])
+                self.util.copy_flags(d, self.df_test)
+            store = self.util.get_path("store")
+            storage_test = f"{store}testdf.csv"
+            storage_train = f"{store}traindf.csv"
+            self.df_test.to_csv(storage_test)
+            self.df_train.to_csv(storage_train)
+
+        self.util.copy_flags(self, self.df_test)
+        self.util.copy_flags(self, self.df_train)
+        # Try data checks
+        datachecker = FileChecker(self.df_train)
+        self.df_train = datachecker.all_checks()
+        datachecker.set_data(self.df_test)
+        self.df_test = datachecker.all_checks()
+
+        # Check for filters
+        filter_sample_selection = self.util.config_val(
+            "DATA", "filter.sample_selection", "all"
+        )
+        if filter_sample_selection == "all":
+            datafilter = DataFilter(self.df_train)
+            self.df_train = datafilter.all_filters()
+            datafilter = DataFilter(self.df_test)
+            self.df_test = datafilter.all_filters()
+        elif filter_sample_selection == "train":
+            datafilter = DataFilter(self.df_train)
+            self.df_train = datafilter.all_filters()
+        elif filter_sample_selection == "test":
+            datafilter = DataFilter(self.df_test)
+            self.df_test = datafilter.all_filters()
+        else:
+            self.util.error(
+                "unkown filter sample selection specifier"
+                f" {filter_sample_selection}, should be [all | train | test]"
+            )
+
+        # encode the labels
+        if self.util.exp_is_classification():
+            datatype = self.util.config_val("DATA", "type", "dummy")
+            if datatype == "continuous":
+                # if self.df_test.is_labeled:
+                #     # remember the target in case they get labelencoded later
+                #     self.df_test["class_label"] = self.df_test[self.target]
+                test_cats = self.df_test["class_label"].unique()
+                # else:
+                #     # if there is no target, copy a dummy label
+                #     self.df_test = self._add_random_target(self.df_test)
+                # if self.df_train.is_labeled:
+                #     # remember the target in case they get labelencoded later
+                #     self.df_train["class_label"] = self.df_train[self.target]
+                train_cats = self.df_train["class_label"].unique()
+
+            else:
+                if self.df_test.is_labeled:
+                    test_cats = self.df_test[self.target].unique()
+                else:
+                    # if there is no target, copy a dummy label
+                    self.df_test = self._add_random_target(self.df_test).astype("str")
+                train_cats = self.df_train[self.target].unique()
+                # print(f"df_train: {pd.DataFrame(self.df_train[self.target])}")
+                # print(f"train_cats with target {self.target}: {train_cats}")
+            if self.df_test.is_labeled:
+                if type(test_cats) == np.ndarray:
+                    self.util.debug(f"Categories test (nd.array): {test_cats}")
+                else:
+                    self.util.debug(f"Categories test (list): {list(test_cats)}")
+            if type(train_cats) == np.ndarray:
+                self.util.debug(f"Categories train (nd.array): {train_cats}")
+            else:
+                self.util.debug(f"Categories train (list): {list(train_cats)}")
+
+            # encode the labels as numbers
+            self.label_encoder = LabelEncoder()
+            self.df_train[self.target] = self.label_encoder.fit_transform(
+                self.df_train[self.target]
+            )
+            self.df_test[self.target] = self.label_encoder.transform(
+                self.df_test[self.target]
+            )
+            glob_conf.set_label_encoder(self.label_encoder)
+        if self.got_speaker:
+            self.util.debug(
+                f"{self.df_test.speaker.nunique()} speakers in test and"
+                f" {self.df_train.speaker.nunique()} speakers in train"
+            )
+
+        target_factor = self.util.config_val("DATA", "target_divide_by", False)
+        if target_factor:
+            self.df_test[self.target] = self.df_test[self.target] / float(target_factor)
+            self.df_train[self.target] = self.df_train[self.target] / float(
+                target_factor
+            )
+            if not self.util.exp_is_classification():
+                self.df_test["class_label"] = self.df_test["class_label"] / float(
+                    target_factor
+                )
+                self.df_train["class_label"] = self.df_train["class_label"] / float(
+                    target_factor
+                )
+
+    def _add_random_target(self, df):
+        labels = glob_conf.labels
+        a = [None] * len(df)
+        for i in range(0, len(df)):
+            a[i] = random.choice(labels)
+        df[self.target] = a
+        return df
+
+    def plot_distribution(self, df_labels):
+        """Plot the distribution of samples and speaker per target class and biological sex"""
+        plot = Plots()
+        sample_selection = self.util.config_val("EXPL", "sample_selection", "all")
+        plot.plot_distributions(df_labels)
+        if self.got_speaker:
+            plot.plot_distributions_speaker(df_labels)
+
+    def extract_test_feats(self):
+        self.feats_test = pd.DataFrame()
+        feats_name = "_".join(ast.literal_eval(glob_conf.config["DATA"]["tests"]))
+        feats_types = self.util.config_val_list("FEATS", "type", ["os"])
+        self.feature_extractor = FeatureExtractor(
+            self.df_test, feats_types, feats_name, "test"
+        )
+        self.feats_test = self.feature_extractor.extract()
+        self.util.debug(f"Test features shape:{self.feats_test.shape}")
+
+    def extract_feats(self):
+        """Extract the features for train and dev sets.
+
+        They will be stored on disk and need to be removed manually.
+
+        The string FEATS.feats_type is read from the config, defaults to os.
+
+        """
+        df_train, df_test = self.df_train, self.df_test
+        feats_name = "_".join(ast.literal_eval(glob_conf.config["DATA"]["databases"]))
+        self.feats_test, self.feats_train = pd.DataFrame(), pd.DataFrame()
+        feats_types = self.util.config_val_list("FEATS", "type", ["os"])
+        self.feature_extractor = FeatureExtractor(
+            df_train, feats_types, feats_name, "train"
+        )
+        self.feats_train = self.feature_extractor.extract()
+        self.feature_extractor = FeatureExtractor(
+            df_test, feats_types, feats_name, "test"
+        )
+        self.feats_test = self.feature_extractor.extract()
+        self.util.debug(
+            f"All features: train shape : {self.feats_train.shape}, test"
+            f" shape:{self.feats_test.shape}"
+        )
+        if self.feats_train.shape[0] < self.df_train.shape[0]:
+            self.util.warn(
+                f"train feats ({self.feats_train.shape[0]}) != train labels"
+                f" ({self.df_train.shape[0]})"
+            )
+            self.df_train = self.df_train[
+                self.df_train.index.isin(self.feats_train.index)
+            ]
+            self.util.warn(f"new train labels shape: {self.df_train.shape[0]}")
+        if self.feats_test.shape[0] < self.df_test.shape[0]:
+            self.util.warn(
+                f"test feats ({self.feats_test.shape[0]}) != test labels"
+                f" ({self.df_test.shape[0]})"
+            )
+            self.df_test = self.df_test[self.df_test.index.isin(self.feats_test.index)]
+            self.util.warn(f"mew test labels shape: {self.df_test.shape[0]}")
+
+        self._check_scale()
+
+    def augment(self):
+        """
+        Augment the selected samples
+        """
+        from nkululeko.augmenting.augmenter import Augmenter
+
+        sample_selection = self.util.config_val("AUGMENT", "sample_selection", "all")
+        if sample_selection == "all":
+            df = pd.concat([self.df_train, self.df_test])
+        elif sample_selection == "train":
+            df = self.df_train
+        elif sample_selection == "test":
+            df = self.df_test
+        else:
+            self.util.error(
+                f"unknown augmentation selection specifier {sample_selection},"
+                " should be [all | train | test]"
+            )
+
+        augmenter = Augmenter(df)
+        df_ret = augmenter.augment(sample_selection)
+        return df_ret
+
+    def autopredict(self):
+        """
+        Predict labels for samples with existing models and add to the dataframe.
+        """
+        sample_selection = self.util.config_val("PREDICT", "split", "all")
+        if sample_selection == "all":
+            df = pd.concat([self.df_train, self.df_test])
+        elif sample_selection == "train":
+            df = self.df_train
+        elif sample_selection == "test":
+            df = self.df_test
+        else:
+            self.util.error(
+                f"unknown augmentation selection specifier {sample_selection},"
+                " should be [all | train | test]"
+            )
+        targets = self.util.config_val_list("PREDICT", "targets", ["gender"])
+        for target in targets:
+            if target == "gender":
+                from nkululeko.autopredict.ap_gender import GenderPredictor
+
+                predictor = GenderPredictor(df)
+                df = predictor.predict(sample_selection)
+            elif target == "age":
+                from nkululeko.autopredict.ap_age import AgePredictor
+
+                predictor = AgePredictor(df)
+                df = predictor.predict(sample_selection)
+            elif target == "snr":
+                from nkululeko.autopredict.ap_snr import SNRPredictor
+
+                predictor = SNRPredictor(df)
+                df = predictor.predict(sample_selection)
+            elif target == "mos":
+                from nkululeko.autopredict.ap_mos import MOSPredictor
+
+                predictor = MOSPredictor(df)
+                df = predictor.predict(sample_selection)
+            elif target == "pesq":
+                from nkululeko.autopredict.ap_pesq import PESQPredictor
+
+                predictor = PESQPredictor(df)
+                df = predictor.predict(sample_selection)
+            elif target == "sdr":
+                from nkululeko.autopredict.ap_sdr import SDRPredictor
+
+                predictor = SDRPredictor(df)
+                df = predictor.predict(sample_selection)
+            elif target == "stoi":
+                from nkululeko.autopredict.ap_stoi import STOIPredictor
+
+                predictor = STOIPredictor(df)
+                df = predictor.predict(sample_selection)
+            elif target == "arousal":
+                from nkululeko.autopredict.ap_arousal import ArousalPredictor
+
+                predictor = ArousalPredictor(df)
+                df = predictor.predict(sample_selection)
+            elif target == "valence":
+                from nkululeko.autopredict.ap_valence import ValencePredictor
+
+                predictor = ValencePredictor(df)
+                df = predictor.predict(sample_selection)
+            elif target == "dominance":
+                from nkululeko.autopredict.ap_dominance import DominancePredictor
+
+                predictor = DominancePredictor(df)
+                df = predictor.predict(sample_selection)
+            else:
+                self.util.error(f"unknown auto predict target: {target}")
+        return df
+
+    def random_splice(self):
+        """
+        Random-splice the selected samples
+        """
+        from nkululeko.augmenting.randomsplicer import Randomsplicer
+
+        sample_selection = self.util.config_val("AUGMENT", "sample_selection", "all")
+        if sample_selection == "all":
+            df = pd.concat([self.df_train, self.df_test])
+        elif sample_selection == "train":
+            df = self.df_train
+        elif sample_selection == "test":
+            df = self.df_test
+        else:
+            self.util.error(
+                f"unknown augmentation selection specifier {sample_selection},"
+                " should be [all | train | test]"
+            )
+        randomsplicer = Randomsplicer(df)
+        df_ret = randomsplicer.run(sample_selection)
+        return df_ret
+
+    def analyse_features(self, needs_feats):
+        """Do a feature exploration."""
+        plot_feats = eval(
+            self.util.config_val("EXPL", "feature_distributions", "False")
+        )
+        sample_selection = self.util.config_val("EXPL", "sample_selection", "all")
+        # get the data labels
+        if sample_selection == "all":
+            df_labels = pd.concat([self.df_train, self.df_test])
+            self.util.copy_flags(self.df_train, df_labels)
+        elif sample_selection == "train":
+            df_labels = self.df_train
+            self.util.copy_flags(self.df_train, df_labels)
+        elif sample_selection == "test":
+            df_labels = self.df_test
+            self.util.copy_flags(self.df_test, df_labels)
+        else:
+            self.util.error(
+                f"unknown sample selection specifier {sample_selection}, should"
+                " be [all | train | test]"
+            )
+        self.util.debug(f"sampling selection: {sample_selection}")
+        if self.util.config_val("EXPL", "value_counts", False):
+            self.plot_distribution(df_labels)
+
+        # check if data should be shown with the spotlight data visualizer
+        spotlight = eval(self.util.config_val("EXPL", "spotlight", "False"))
+        if spotlight:
+            self.util.debug("opening spotlight tab in web browser")
+            from renumics import spotlight
+
+            spotlight.show(df_labels.reset_index())
+
+        if not needs_feats:
+            return
+        # get the feature values
+        if sample_selection == "all":
+            df_feats = pd.concat([self.feats_train, self.feats_test])
+        elif sample_selection == "train":
+            df_feats = self.feats_train
+        elif sample_selection == "test":
+            df_feats = self.feats_test
+        else:
+            self.util.error(
+                f"unknown sample selection specifier {sample_selection}, should"
+                " be [all | train | test]"
+            )
+        feat_analyser = FeatureAnalyser(sample_selection, df_labels, df_feats)
+        # check if SHAP features should be analysed
+        shap = eval(self.util.config_val("EXPL", "shap", "False"))
+        if shap:
+            feat_analyser.analyse_shap(self.runmgr.get_best_model())
+
+        if plot_feats:
+            feat_analyser.analyse()
+
+        # check if a scatterplot should be done
+        scatter_var = eval(self.util.config_val("EXPL", "scatter", "False"))
+        scatter_target = self.util.config_val(
+            "EXPL", "scatter.target", "['class_label']"
+        )
+        if scatter_var:
+            scatters = ast.literal_eval(glob_conf.config["EXPL"]["scatter"])
+            scat_targets = ast.literal_eval(scatter_target)
+            plots = Plots()
+            for scat_target in scat_targets:
+                if self.util.is_categorical(df_labels[scat_target]):
+                    for scatter in scatters:
+                        plots.scatter_plot(df_feats, df_labels, scat_target, scatter)
+                else:
+                    self.util.debug(
+                        f"{self.name}: binning continuous variable to categories"
+                    )
+                    cat_vals = self.util.continuous_to_categorical(
+                        df_labels[scat_target]
+                    )
+                    df_labels[f"{scat_target}_bins"] = cat_vals.values
+                    for scatter in scatters:
+                        plots.scatter_plot(
+                            df_feats, df_labels, f"{scat_target}_bins", scatter
+                        )
+
+    def _check_scale(self):
+        scale_feats = self.util.config_val("FEATS", "scale", False)
+        # print the scale
+        self.util.debug(f"scaler: {scale_feats}")
+        if scale_feats:
+            self.scaler_feats = Scaler(
+                self.df_train,
+                self.df_test,
+                self.feats_train,
+                self.feats_test,
+                scale_feats,
+            )
+            self.feats_train, self.feats_test = self.scaler_feats.scale()
+            # store versions
+            self.util.save_to_store(self.feats_train, "feats_train_scaled")
+            self.util.save_to_store(self.feats_test, "feats_test_scaled")
+
+    def init_runmanager(self):
+        """Initialize the manager object for the runs."""
+        self.runmgr = Runmanager(
+            self.df_train, self.df_test, self.feats_train, self.feats_test
+        )
+
+    def run(self):
+        """Do the runs."""
+        self.runmgr.do_runs()
+
+        # access the best results all runs
+        self.reports = self.runmgr.best_results
+        last_epochs = self.runmgr.last_epochs
+        # try to save yourself
+        save = self.util.config_val("EXP", "save", False)
+        if save:
+            # save the experiment for future use
+            self.save(self.util.get_save_name())
+            # self.save_onnx(self.util.get_save_name())
+
+        # self.__collect_reports()
+        self.util.print_best_results(self.reports)
+
+        # check if the test predictions should be saved to disk
+        test_pred_file = self.util.config_val("EXP", "save_test", False)
+        if test_pred_file:
+            self.predict_test_and_save(test_pred_file)
+
+        # check if the majority voting for all speakers should be plotted
+        conf_mat_per_speaker_function = self.util.config_val(
+            "PLOT", "combine_per_speaker", False
+        )
+        if conf_mat_per_speaker_function:
+            self.plot_confmat_per_speaker(conf_mat_per_speaker_function)
+        used_time = time.process_time() - self.start
+        self.util.debug(f"Done, used {used_time:.3f} seconds")
+
+        # check if a test set should be labeled by the model:
+        label_data = self.util.config_val("DATA", "label_data", False)
+        label_result = self.util.config_val("DATA", "label_result", False)
+        if label_data and label_result:
+            self.predict_test_and_save(label_result)
+
+        return self.reports, last_epochs
+
+    def plot_confmat_per_speaker(self, function):
+        if self.loso or self.logo or self.xfoldx:
+            self.util.debug(
+                "plot combined speaker predictions not possible for cross" " validation"
+            )
+            return
+        best = self.get_best_report(self.reports)
+        # if not best.is_classification:
+        #     best.continuous_to_categorical()
+        truths = best.truths
+        preds = best.preds
+        speakers = self.df_test.speaker.values
+        print(f"{len(truths)} {len(preds)} {len(speakers) }")
+        df = pd.DataFrame(data={"truth": truths, "pred": preds, "speaker": speakers})
+        plot_name = "result_combined_per_speaker"
+        self.util.debug(
+            f"plotting speaker combination ({function}) confusion matrix to"
+            f" {plot_name}"
+        )
+        best.plot_per_speaker(df, plot_name, function)
+
+    def get_best_report(self, reports):
+        return self.runmgr.get_best_result(reports)
+
+    def print_best_model(self):
+        self.runmgr.print_best_result_runs()
+
+    def demo(self, file, is_list, outfile):
+        model = self.runmgr.get_best_model()
+        labelEncoder = None
+        try:
+            labelEncoder = self.label_encoder
+        except AttributeError:
+            pass
+        demo = Demo_predictor(
+            model, file, is_list, self.feature_extractor, labelEncoder, outfile
+        )
+        demo.run_demo()
+
+    def predict_test_and_save(self, result_name):
+        model = self.runmgr.get_best_model()
+        model.set_testdata(self.df_test, self.feats_test)
+        test_predictor = TestPredictor(
+            model, self.df_test, self.label_encoder, result_name
+        )
+        result = test_predictor.predict_and_store()
+        return result
+
+    def load(self, filename):
+        try:
+            f = open(filename, "rb")
+            tmp_dict = pickle.load(f)
+            f.close()
+        except EOFError as eof:
+            self.util.error(f"can't open file {filename}: {eof}")
+        self.__dict__.update(tmp_dict)
+        glob_conf.set_labels(self.labels)
+
+    def save(self, filename):
+        if self.runmgr.modelrunner.model.is_ann():
+            self.runmgr.modelrunner.model = None
+            self.util.warn(
+                "Save experiment: Can't pickle the trained model so saving without it. (it should be stored anyway)"
+            )
+        try:
+            f = open(filename, "wb")
+            pickle.dump(self.__dict__, f)
+            f.close()
+        except (TypeError, AttributeError) as error:
+            self.feature_extractor.feat_extractor.model = None
+            f = open(filename, "wb")
+            pickle.dump(self.__dict__, f)
+            f.close()
+            self.util.warn(
+                "Save experiment: Can't pickle the feature extraction model so saving without it."
+                + f"{type(error).__name__} {error}"
+            )
+        except RuntimeError as error:
+            self.util.warn(
+                "Save experiment: Can't pickle local object, NOT saving: "
+                + f"{type(error).__name__} {error}"
+            )
+
+    def save_onnx(self, filename):
+        # export the model to onnx
+        model = self.runmgr.get_best_model()
+        if model.is_ann():
+            print("converting to onnx from torch")
+        else:
+
+            print("converting to onnx from sklearn")
+        # save the rest
+        f = open(filename, "wb")
+        pickle.dump(self.__dict__, f)
+        f.close()
diff --git a/nkululeko/explore.py b/nkululeko/explore.py
index 06dc21c8..919ce254 100644
--- a/nkululeko/explore.py
+++ b/nkululeko/explore.py
@@ -19,6 +19,7 @@
 the configuration file to use. If no configuration file is provided, it will look
 for an `exp.ini` file in the same directory as the script.
 """
+
 # explore.py
 # explore the feature sets
 
diff --git a/nkululeko/export.py b/nkululeko/export.py
index 0fb76970..43834626 100644
--- a/nkululeko/export.py
+++ b/nkululeko/export.py
@@ -1,16 +1,18 @@
 # export.py
 # export the loaded training and test sets to it's own folder
 
-import os
-import pandas as pd
+import argparse
 import configparser
+import os
+import shutil
+
 import audeer
-import argparse
 import audiofile
+import pandas as pd
+
+from nkululeko.constants import VERSION
 from nkululeko.experiment import Experiment
 from nkululeko.utils.util import Util
-from nkululeko.constants import VERSION
-import shutil
 
 
 def main(src_dir):
diff --git a/nkululeko/feat_extract/feats_agender.py b/nkululeko/feat_extract/feats_agender.py
index 15992d8b..b91ad5bf 100644
--- a/nkululeko/feat_extract/feats_agender.py
+++ b/nkululeko/feat_extract/feats_agender.py
@@ -1,16 +1,17 @@
 # feats_agender.py
 
-from nkululeko.feat_extract.featureset import Featureset
 import os
 
 # import pandas as pd
 import audeer
-import nkululeko.glob_conf as glob_conf
+import audinterface
 import audonnx
 import numpy as np
-import audinterface
 import torch
 
+import nkululeko.glob_conf as glob_conf
+from nkululeko.feat_extract.featureset import Featureset
+
 
 class AgenderSet(Featureset):
     """
@@ -37,7 +38,7 @@ def _load_model(self):
         cuda = "cuda" if torch.cuda.is_available() else "cpu"
         device = self.util.config_val("MODEL", "device", cuda)
         self.model = audonnx.load(model_root, device=device)
-        self.util.debug(f"initialized agender model")
+        self.util.debug("initialized agender model")
         self.model_loaded = True
 
     def extract(self):
diff --git a/nkululeko/feat_extract/feats_agender_agender.py b/nkululeko/feat_extract/feats_agender_agender.py
index 83a3a162..5c495925 100644
--- a/nkululeko/feat_extract/feats_agender_agender.py
+++ b/nkululeko/feat_extract/feats_agender_agender.py
@@ -1,14 +1,16 @@
 # feats_audmodel_dim.py
-from nkululeko.feat_extract.featureset import Featureset
 import os
-import pandas as pd
+
 import audeer
-import nkululeko.glob_conf as glob_conf
+import audinterface
 import audonnx
 import numpy as np
-import audinterface
 import torch
 
+import nkululeko.glob_conf as glob_conf
+from nkululeko.feat_extract.featureset import Featureset
+
+
 class Agender_agenderSet(Featureset):
     """
     Age and gender predictions from the wav2vec2. based model finetuned on agender, described in the paper
@@ -29,8 +31,7 @@ def _load_model(self):
         if not os.path.isdir(model_root):
             cache_root = audeer.mkdir("cache")
             model_root = audeer.mkdir(model_root)
-            archive_path = audeer.download_url(
-                model_url, cache_root, verbose=True)
+            archive_path = audeer.download_url(model_url, cache_root, verbose=True)
             audeer.extract_archive(archive_path, model_root)
         cuda = "cuda" if torch.cuda.is_available() else "cpu"
         device = self.util.config_val("MODEL", "device", cuda)
diff --git a/nkululeko/feat_extract/feats_analyser.py b/nkululeko/feat_extract/feats_analyser.py
index 0859b1b4..809d9da4 100644
--- a/nkululeko/feat_extract/feats_analyser.py
+++ b/nkululeko/feat_extract/feats_analyser.py
@@ -1,18 +1,18 @@
 # feats_analyser.py
 import ast
+
+import matplotlib.pyplot as plt
 import pandas as pd
 from sklearn.inspection import permutation_importance
-from sklearn.linear_model import LogisticRegression
-from sklearn.tree import DecisionTreeClassifier
-from sklearn.linear_model import LinearRegression
-from sklearn.tree import DecisionTreeRegressor
-import matplotlib.pyplot as plt
-from nkululeko.utils.util import Util
-from nkululeko.utils.stats import normalize
-from nkululeko.plots import Plots
+from sklearn.linear_model import LinearRegression, LogisticRegression
+from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
+
 import nkululeko.glob_conf as glob_conf
-from nkululeko.reporting.report_item import ReportItem
+from nkululeko.plots import Plots
 from nkululeko.reporting.defines import Header
+from nkululeko.reporting.report_item import ReportItem
+from nkululeko.utils.stats import normalize
+from nkululeko.utils.util import Util
 
 
 class FeatureAnalyser:
@@ -58,9 +58,9 @@ def analyse_shap(self, model):
                 model_func = model.clf.predict
             else:
                 raise Exception("Model not supported for SHAP analysis")
-            
+
             self.util.debug(f"using SHAP explainer for {model_name} model")
-            
+
             explainer = shap.Explainer(
                 model_func,
                 self.features,
@@ -68,14 +68,14 @@ def analyse_shap(self, model):
                 algorithm="permutation",
                 npermutations=5,
             )
- 
+
             self.util.debug("computing SHAP values...")
             shap_values = explainer(self.features)
             self.util.to_pickle(shap_values, name)
         else:
             shap_values = self.util.from_pickle(name)
         # plt.figure()
-        plt.close('all')
+        plt.close("all")
         plt.tight_layout()
         shap.plots.bar(shap_values)
         fig_dir = self.util.get_path("fig_dir") + "../"  # one up because of the runs
@@ -272,7 +272,7 @@ def analyse(self):
         fig = ax.figure
         fig.clear()
         plt.close(fig)
-        caption = f"Feature importance"
+        caption = "Feature importance"
         if permutation:
             caption += " based on permutation of features."
         glob_conf.report.add_item(
@@ -285,7 +285,9 @@ def analyse(self):
         )
 
         # print feature importance values to file and debug and save to result
-        self.util.debug(f"Importance features from {model_name}: features = \n{df_imp['feats'].values.tolist()}")
+        self.util.debug(
+            f"Importance features from {model_name}: features = \n{df_imp['feats'].values.tolist()}"
+        )
         # result file
         res_dir = self.util.get_path("res_dir")
         filename = f"_EXPL_{model_name}"
@@ -300,7 +302,7 @@ def analyse(self):
 
         df_imp.to_csv(filename, mode="a")
         self.util.debug(f"Saved feature importance values to {filename}")
-        
+
         # check if feature distributions should be plotted
         plot_feats = self.util.config_val("EXPL", "feature_distributions", False)
         if plot_feats:
diff --git a/nkululeko/feat_extract/feats_ast.py b/nkululeko/feat_extract/feats_ast.py
index 816f56f4..132806a7 100644
--- a/nkululeko/feat_extract/feats_ast.py
+++ b/nkululeko/feat_extract/feats_ast.py
@@ -4,10 +4,9 @@
 import numpy as np
 import pandas as pd
 import torch
-import torch.nn.functional as F
 import torchaudio
 from tqdm import tqdm
-from transformers import AutoProcessor, ASTModel
+from transformers import ASTModel, AutoProcessor
 
 import nkululeko.glob_conf as glob_conf
 from nkululeko.feat_extract.featureset import Featureset
@@ -34,7 +33,6 @@ def init_model(self):
         self.model.eval()
         self.model_initialized = True
 
-
     def extract(self):
         """Extract the features or load them from disk if present."""
         store = self.util.get_path("store")
@@ -80,11 +78,12 @@ def extract(self):
                     f"got nan: {self.df.shape} {self.df.isnull().sum().sum()}"
                 )
 
-
     def get_embeddings(self, signal, sampling_rate, file):
         """Extract embeddings from raw audio signal."""
         try:
-            inputs = self.processor(signal.numpy(), sampling_rate=sampling_rate, return_tensors="pt")
+            inputs = self.processor(
+                signal.numpy(), sampling_rate=sampling_rate, return_tensors="pt"
+            )
 
             inputs = {k: v.to(self.device) for k, v in inputs.items()}
 
@@ -102,14 +101,16 @@ def get_embeddings(self, signal, sampling_rate, file):
 
             # print(f"hs shape: {embeddings.shape}")
             # hs shape: (1, 768)
-        
+
         except Exception as e:
-            self.util.error(f"Error extracting embeddings for file {file}: {str(e)}, fill with")
+            self.util.error(
+                f"Error extracting embeddings for file {file}: {str(e)}, fill with"
+            )
             return np.zeros(
                 self.model.config.hidden_size
             )  # Return zero vector on error
         return embeddings.ravel()
-    
+
     def extract_sample(self, signal, sr):
         self.init_model()
         feats = self.get_embeddings(signal, sr, "no file")
diff --git a/nkululeko/feat_extract/feats_auddim.py b/nkululeko/feat_extract/feats_auddim.py
index fe4f8262..389f758f 100644
--- a/nkululeko/feat_extract/feats_auddim.py
+++ b/nkululeko/feat_extract/feats_auddim.py
@@ -1,16 +1,14 @@
 # feats_audmodel_dim.py
 import os
 
-import numpy as np
-import pandas as pd
-import torch
-
 import audeer
 import audinterface
 import audonnx
+import numpy as np
+import torch
 
-from nkululeko.feat_extract.featureset import Featureset
 import nkululeko.glob_conf as glob_conf
+from nkululeko.feat_extract.featureset import Featureset
 
 
 class AuddimSet(Featureset):
diff --git a/nkululeko/feat_extract/feats_audmodel.py b/nkululeko/feat_extract/feats_audmodel.py
index 74c783a1..856969d8 100644
--- a/nkululeko/feat_extract/feats_audmodel.py
+++ b/nkululeko/feat_extract/feats_audmodel.py
@@ -4,10 +4,10 @@
 import audeer
 import audinterface
 import audonnx
-import nkululeko.glob_conf as glob_conf
 import numpy as np
-import pandas as pd
 import torch
+
+import nkululeko.glob_conf as glob_conf
 from nkululeko.feat_extract.featureset import Featureset
 
 
diff --git a/nkululeko/feat_extract/feats_clap.py b/nkululeko/feat_extract/feats_clap.py
index da1f5de4..dbe84af6 100644
--- a/nkululeko/feat_extract/feats_clap.py
+++ b/nkululeko/feat_extract/feats_clap.py
@@ -1,14 +1,14 @@
 # feats_clap.py
 
-from nkululeko.utils.util import Util
-from nkululeko.feat_extract.featureset import Featureset
 import os
+
+import audiofile
+import laion_clap
 import pandas as pd
 from tqdm import tqdm
-import os
+
 import nkululeko.glob_conf as glob_conf
-import laion_clap
-import audiofile
+from nkululeko.feat_extract.featureset import Featureset
 
 
 class ClapSet(Featureset):
@@ -26,21 +26,19 @@ def init_model(self):
         self.util.debug("loading clap model...")
         self.model = laion_clap.CLAP_Module(enable_fusion=False)
         self.model.load_ckpt()  # download the default pretrained checkpoint.
-        print(f"loaded clap model")
+        print("loaded clap model")
 
     def extract(self):
         """Extract the features or load them from disk if present."""
         store = self.util.get_path("store")
         store_format = self.util.config_val("FEATS", "store_format", "pkl")
         storage = f"{store}{self.name}.{store_format}"
-        extract = self.util.config_val(
-            "FEATS", "needs_feature_extraction", False)
+        extract = self.util.config_val("FEATS", "needs_feature_extraction", False)
         no_reuse = eval(self.util.config_val("FEATS", "no_reuse", "False"))
         if extract or no_reuse or not os.path.isfile(storage):
             if not self.model_initialized:
                 self.init_model()
-            self.util.debug(
-                "extracting clap embeddings, this might take a while...")
+            self.util.debug("extracting clap embeddings, this might take a while...")
             emb_series = pd.Series(index=self.data_df.index, dtype=object)
             length = len(self.data_df.index)
             for idx, (file, start, end) in enumerate(
@@ -54,8 +52,7 @@ def extract(self):
                 )
                 emb = self.get_embeddings(signal, sampling_rate)
                 emb_series[idx] = emb
-            self.df = pd.DataFrame(
-                emb_series.values.tolist(), index=self.data_df.index)
+            self.df = pd.DataFrame(emb_series.values.tolist(), index=self.data_df.index)
             self.util.write_store(self.df, storage, store_format)
             try:
                 glob_conf.config["DATA"]["needs_feature_extraction"] = "false"
diff --git a/nkululeko/feat_extract/feats_hubert.py b/nkululeko/feat_extract/feats_hubert.py
index 99c07b94..101081f9 100644
--- a/nkululeko/feat_extract/feats_hubert.py
+++ b/nkululeko/feat_extract/feats_hubert.py
@@ -10,11 +10,10 @@
 import torch
 import torchaudio
 from tqdm import tqdm
-from transformers import HubertModel
-from transformers import Wav2Vec2FeatureExtractor
+from transformers import HubertModel, Wav2Vec2FeatureExtractor
 
-from nkululeko.feat_extract.featureset import Featureset
 import nkululeko.glob_conf as glob_conf
+from nkululeko.feat_extract.featureset import Featureset
 
 
 class Hubert(Featureset):
diff --git a/nkululeko/feat_extract/feats_import.py b/nkululeko/feat_extract/feats_import.py
index 73fc5f32..5e939daf 100644
--- a/nkululeko/feat_extract/feats_import.py
+++ b/nkululeko/feat_extract/feats_import.py
@@ -1,10 +1,11 @@
 # feats_import.py
 
-import os
 import ast
+import os
+
 import audformat
 import pandas as pd
-from nkululeko.utils.util import Util
+
 from nkululeko.feat_extract.featureset import Featureset
 
 
@@ -20,11 +21,11 @@ def extract(self):
         try:
             feat_import_files = self.util.config_val("FEATS", "import_file", False)
             feat_import_files = ast.literal_eval(feat_import_files)
-        except ValueError as e:
+        except ValueError:
             self.util.error(
                 "feature type == import needs import_file = ['file1', 'filex']"
             )
-        except SyntaxError as se:
+        except SyntaxError:
             if type(feat_import_files) == str:
                 feat_import_files = [feat_import_files]
             else:
diff --git a/nkululeko/feat_extract/feats_mld.py b/nkululeko/feat_extract/feats_mld.py
index 6a17a009..97208fbc 100644
--- a/nkululeko/feat_extract/feats_mld.py
+++ b/nkululeko/feat_extract/feats_mld.py
@@ -1,11 +1,9 @@
 # mld_fset.py
-from nkululeko.feat_extract.featureset import Featureset
-import sys
 import os
-import pandas as pd
-import numpy as np
-from nkululeko.utils.util import Util
+import sys
+
 import nkululeko.glob_conf as glob_conf
+from nkululeko.feat_extract.featureset import Featureset
 
 
 class MLD_set(Featureset):
diff --git a/nkululeko/feat_extract/feats_mos.py b/nkululeko/feat_extract/feats_mos.py
index d2c38f47..b804f0af 100644
--- a/nkululeko/feat_extract/feats_mos.py
+++ b/nkululeko/feat_extract/feats_mos.py
@@ -12,15 +12,16 @@
 """
 
 import os
+
+import audiofile
 import pandas as pd
-from tqdm import tqdm
 import torch
 import torchaudio
 from torchaudio.pipelines import SQUIM_SUBJECTIVE
 from torchaudio.utils import download_asset
-import audiofile
+from tqdm import tqdm
+
 import nkululeko.glob_conf as glob_conf
-from nkululeko.utils.util import Util
 from nkululeko.feat_extract.featureset import Featureset
 
 
diff --git a/nkululeko/feat_extract/feats_opensmile.py b/nkululeko/feat_extract/feats_opensmile.py
index e423a890..49b5e3a9 100644
--- a/nkululeko/feat_extract/feats_opensmile.py
+++ b/nkululeko/feat_extract/feats_opensmile.py
@@ -1,10 +1,11 @@
 # opensmileset.py
-from nkululeko.feat_extract.featureset import Featureset
 import os
+
+import opensmile
 import pandas as pd
+
 import nkululeko.glob_conf as glob_conf
-import ast
-import opensmile
+from nkululeko.feat_extract.featureset import Featureset
 
 
 class Opensmileset(Featureset):
diff --git a/nkululeko/feat_extract/feats_oxbow.py b/nkululeko/feat_extract/feats_oxbow.py
index c679a0fe..6a3d875f 100644
--- a/nkululeko/feat_extract/feats_oxbow.py
+++ b/nkululeko/feat_extract/feats_oxbow.py
@@ -1,10 +1,11 @@
 # feats_oxbow.py
 
-from nkululeko.utils.util import Util
-from nkululeko.feat_extract.featureset import Featureset
 import os
-import pandas as pd
+
 import opensmile
+import pandas as pd
+
+from nkululeko.feat_extract.featureset import Featureset
 
 
 class Openxbow(Featureset):
@@ -53,7 +54,7 @@ def extract(self):
             # check if JAR file exist
             if not os.path.isfile(f"{xbow_path}/openXBOW.jar"):
                 # download using wget if not exist and locate in xbow_path
-                os.system(f"git clone https://github.com/openXBOW/openXBOW")
+                os.system("git clone https://github.com/openXBOW/openXBOW")
             # get the size of the codebook
             size = self.util.config_val("FEATS", "size", 500)
             # get the number of assignements
diff --git a/nkululeko/feat_extract/feats_praat.py b/nkululeko/feat_extract/feats_praat.py
index 6bc69049..972a5681 100644
--- a/nkululeko/feat_extract/feats_praat.py
+++ b/nkululeko/feat_extract/feats_praat.py
@@ -1,13 +1,12 @@
 # feats_praat.py
-import ast
 import os
 
 import numpy as np
 import pandas as pd
 
+import nkululeko.glob_conf as glob_conf
 from nkululeko.feat_extract import feinberg_praat
 from nkululeko.feat_extract.featureset import Featureset
-import nkululeko.glob_conf as glob_conf
 
 
 class PraatSet(Featureset):
@@ -26,12 +25,10 @@ def extract(self):
         store = self.util.get_path("store")
         store_format = self.util.config_val("FEATS", "store_format", "pkl")
         storage = f"{store}{self.name}.{store_format}"
-        extract = self.util.config_val(
-            "FEATS", "needs_feature_extraction", False)
+        extract = self.util.config_val("FEATS", "needs_feature_extraction", False)
         no_reuse = eval(self.util.config_val("FEATS", "no_reuse", "False"))
         if extract or no_reuse or not os.path.isfile(storage):
-            self.util.debug(
-                "extracting Praat features, this might take a while...")
+            self.util.debug("extracting Praat features, this might take a while...")
             self.df = feinberg_praat.compute_features(self.data_df.index)
             self.df = self.df.set_index(self.data_df.index)
             for i, col in enumerate(self.df.columns):
@@ -54,8 +51,8 @@ def extract(self):
         self.df = self.df.astype(float)
 
     def extract_sample(self, signal, sr):
-        import audiofile
         import audformat
+        import audiofile
 
         tmp_audio_names = ["praat_audio_tmp.wav"]
         audiofile.write(tmp_audio_names[0], signal, sr)
diff --git a/nkululeko/feat_extract/feats_snr.py b/nkululeko/feat_extract/feats_snr.py
index 5077be96..fb1dad79 100644
--- a/nkululeko/feat_extract/feats_snr.py
+++ b/nkululeko/feat_extract/feats_snr.py
@@ -2,6 +2,7 @@
 
 SNR (signal to noise ratio) is extracted as acoustic features.
 """
+
 import os
 
 import audiofile
@@ -11,7 +12,6 @@
 import nkululeko.glob_conf as glob_conf
 from nkululeko.autopredict.estimate_snr import SNREstimator
 from nkululeko.feat_extract.featureset import Featureset
-from nkululeko.utils.util import Util
 
 
 class SNRSet(Featureset):
@@ -28,8 +28,7 @@ def extract(self):
         store = self.util.get_path("store")
         store_format = self.util.config_val("FEATS", "store_format", "pkl")
         storage = f"{store}{self.name}.{store_format}"
-        extract = self.util.config_val(
-            "FEATS", "needs_feature_extraction", False)
+        extract = self.util.config_val("FEATS", "needs_feature_extraction", False)
         no_reuse = eval(self.util.config_val("FEATS", "no_reuse", "False"))
         if extract or no_reuse or not os.path.isfile(storage):
             self.util.debug("estimating SNR, this might take a while...")
@@ -46,8 +45,7 @@ def extract(self):
                 snr = self.get_snr(signal[0], sampling_rate)
                 snr_series[idx] = snr
             print("")
-            self.df = pd.DataFrame(
-                snr_series.values.tolist(), index=self.data_df.index)
+            self.df = pd.DataFrame(snr_series.values.tolist(), index=self.data_df.index)
             self.df.columns = ["snr"]
             self.util.write_store(self.df, storage, store_format)
             try:
diff --git a/nkululeko/feat_extract/feats_spectra.py b/nkululeko/feat_extract/feats_spectra.py
index 691fec47..036b45bf 100644
--- a/nkululeko/feat_extract/feats_spectra.py
+++ b/nkululeko/feat_extract/feats_spectra.py
@@ -6,21 +6,20 @@
 """
 
 import os
+import pathlib
+
+import audeer
+import matplotlib.pyplot as plt
+import numpy as np
+import pandas as pd
 import torchaudio
 import torchaudio.transforms as T
-import torch
-from torch.utils.data import Dataset
 from PIL import Image, ImageOps
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
 from tqdm import tqdm
-import pathlib
-import audeer
 
-from nkululeko.feat_extract.featureset import Featureset
-from nkululeko.constants import SAMPLING_RATE
 import nkululeko.glob_conf as glob_conf
+from nkululeko.constants import SAMPLING_RATE
+from nkululeko.feat_extract.featureset import Featureset
 
 
 class Spectraloader(Featureset):
diff --git a/nkululeko/feat_extract/feats_spkrec.py b/nkululeko/feat_extract/feats_spkrec.py
index fb7b9def..a3b971df 100644
--- a/nkululeko/feat_extract/feats_spkrec.py
+++ b/nkululeko/feat_extract/feats_spkrec.py
@@ -7,14 +7,15 @@
 
 import os
 
-import nkululeko.glob_conf as glob_conf
 import pandas as pd
 import torch
 import torchaudio
-from nkululeko.feat_extract.featureset import Featureset
 from speechbrain.inference import EncoderClassifier
 from tqdm import tqdm
 
+import nkululeko.glob_conf as glob_conf
+from nkululeko.feat_extract.featureset import Featureset
+
 # from transformers import HubertModel, Wav2Vec2FeatureExtractor
 
 
@@ -49,16 +50,12 @@ def extract(self):
         """Extract the features or load them from disk if present."""
         store = self.util.get_path("store")
         storage = f"{store}{self.name}.pkl"
-        extract = self.util.config_val(
-            "FEATS", "needs_feature_extraction", False
-        )
+        extract = self.util.config_val("FEATS", "needs_feature_extraction", False)
         no_reuse = eval(self.util.config_val("FEATS", "no_reuse", "False"))
         if extract or no_reuse or not os.path.isfile(storage):
             if not self.classifier_initialized:
                 self.init_model()
-            self.util.debug(
-                "extracting Spkrec embeddings, this might take a while..."
-            )
+            self.util.debug("extracting Spkrec embeddings, this might take a while...")
             emb_series = pd.Series(index=self.data_df.index, dtype=object)
             length = len(self.data_df.index)
             for idx, (file, start, end) in enumerate(
@@ -77,9 +74,7 @@ def extract(self):
                 # fill series with embeddings
                 emb_series.iloc[idx] = emb
             # print(f"emb_series shape: {emb_series.shape}")
-            self.df = pd.DataFrame(
-                emb_series.values.tolist(), index=self.data_df.index
-            )
+            self.df = pd.DataFrame(emb_series.values.tolist(), index=self.data_df.index)
             print(f"df shape: {self.df.shape}")
             self.df.to_pickle(storage)
             try:
diff --git a/nkululeko/feat_extract/feats_squim.py b/nkululeko/feat_extract/feats_squim.py
index b10cb1e4..c7e326bf 100644
--- a/nkululeko/feat_extract/feats_squim.py
+++ b/nkululeko/feat_extract/feats_squim.py
@@ -14,17 +14,15 @@
 
 import os
 
+import audiofile
 import pandas as pd
 import torch
 import torchaudio
 from torchaudio.pipelines import SQUIM_OBJECTIVE
 from tqdm import tqdm
 
-import audiofile
-
-from nkululeko.feat_extract.featureset import Featureset
 import nkululeko.glob_conf as glob_conf
-from nkululeko.utils.util import Util
+from nkululeko.feat_extract.featureset import Featureset
 
 
 class SquimSet(Featureset):
diff --git a/nkululeko/feat_extract/feats_trill.py b/nkululeko/feat_extract/feats_trill.py
index 9f3cc1e4..f6aafeca 100644
--- a/nkululeko/feat_extract/feats_trill.py
+++ b/nkululeko/feat_extract/feats_trill.py
@@ -1,17 +1,14 @@
 # feats_trill.py
 import os
 
+import audiofile as af
 import pandas as pd
 import tensorflow as tf
 import tensorflow_hub as hub
 from tqdm import tqdm
 
-import audiofile as af
-
-from nkululeko.feat_extract.featureset import Featureset
 import nkululeko.glob_conf as glob_conf
-from nkululeko.utils.util import Util
-
+from nkululeko.feat_extract.featureset import Featureset
 
 # Import TF 2.X and make sure we're running eager.
 assert tf.executing_eagerly()
diff --git a/nkululeko/feat_extract/feats_wav2vec2.py b/nkululeko/feat_extract/feats_wav2vec2.py
index 39f5fb9a..f22c770b 100644
--- a/nkululeko/feat_extract/feats_wav2vec2.py
+++ b/nkululeko/feat_extract/feats_wav2vec2.py
@@ -7,14 +7,16 @@
 """
 
 import os
-from tqdm import tqdm
+
 import pandas as pd
 import torch
 import torchaudio
-from transformers import Wav2Vec2FeatureExtractor, Wav2Vec2Model
 import transformers
-from nkululeko.feat_extract.featureset import Featureset
+from tqdm import tqdm
+from transformers import Wav2Vec2FeatureExtractor, Wav2Vec2Model
+
 import nkululeko.glob_conf as glob_conf
+from nkululeko.feat_extract.featureset import Featureset
 
 
 class Wav2vec2(Featureset):
@@ -47,7 +49,9 @@ def init_model(self):
         config.num_hidden_layers = layer_num - hidden_layer
         self.util.debug(f"using hidden layer #{config.num_hidden_layers}")
         self.processor = Wav2Vec2FeatureExtractor.from_pretrained(model_path)
-        self.model = Wav2Vec2Model.from_pretrained(model_path, config=config).to(self.device)
+        self.model = Wav2Vec2Model.from_pretrained(model_path, config=config).to(
+            self.device
+        )
         print(f"intialized Wav2vec model on {self.device}")
         self.model.eval()
         self.model_initialized = True
diff --git a/nkululeko/feat_extract/feats_wavlm.py b/nkululeko/feat_extract/feats_wavlm.py
index e09d0d80..ab841db1 100644
--- a/nkululeko/feat_extract/feats_wavlm.py
+++ b/nkululeko/feat_extract/feats_wavlm.py
@@ -8,11 +8,10 @@
 import torch
 import torchaudio
 from tqdm import tqdm
-from transformers import Wav2Vec2FeatureExtractor
-from transformers import WavLMModel
+from transformers import Wav2Vec2FeatureExtractor, WavLMModel
 
-from nkululeko.feat_extract.featureset import Featureset
 import nkululeko.glob_conf as glob_conf
+from nkululeko.feat_extract.featureset import Featureset
 
 
 class Wavlm(Featureset):
diff --git a/nkululeko/feat_extract/feats_whisper.py b/nkululeko/feat_extract/feats_whisper.py
index b0333bec..cda5a8ea 100644
--- a/nkululeko/feat_extract/feats_whisper.py
+++ b/nkululeko/feat_extract/feats_whisper.py
@@ -1,16 +1,14 @@
 # feats_whisper.py
 import os
 
-import pandas as pd
-import torch
-from transformers import AutoFeatureExtractor
-from transformers import WhisperModel
-
 import audeer
 import audiofile
+import pandas as pd
+import torch
+from transformers import AutoFeatureExtractor, WhisperModel
 
-from nkululeko.feat_extract.featureset import Featureset
 import nkululeko.glob_conf as glob_conf
+from nkululeko.feat_extract.featureset import Featureset
 
 
 class Whisper(Featureset):
diff --git a/nkululeko/feat_extract/featureset.py b/nkululeko/feat_extract/featureset.py
index c797fdc8..fd0d3dde 100644
--- a/nkululeko/feat_extract/featureset.py
+++ b/nkululeko/feat_extract/featureset.py
@@ -1,8 +1,10 @@
 # featureset.py
+import ast
+
 import pandas as pd
-from nkululeko.utils.util import Util
+
 import nkululeko.glob_conf as glob_conf
-import ast
+from nkululeko.utils.util import Util
 
 
 class Featureset:
diff --git a/nkululeko/feat_extract/feinberg_praat.py b/nkululeko/feat_extract/feinberg_praat.py
index cc166207..502e6963 100644
--- a/nkululeko/feat_extract/feinberg_praat.py
+++ b/nkululeko/feat_extract/feinberg_praat.py
@@ -7,6 +7,7 @@
 import math
 import statistics
 
+import audiofile
 import numpy as np
 import pandas as pd
 import parselmouth
@@ -15,9 +16,6 @@
 from sklearn.decomposition import PCA
 from tqdm import tqdm
 
-import audiofile
-
-
 # This is the function to measure source acoustics using default male parameters.
 
 
diff --git a/nkululeko/feat_extract/transformer_feature_extractor.py b/nkululeko/feat_extract/transformer_feature_extractor.py
new file mode 100644
index 00000000..e9134286
--- /dev/null
+++ b/nkululeko/feat_extract/transformer_feature_extractor.py
@@ -0,0 +1,147 @@
+# transformer_feature_extractor.py
+
+import os
+
+import pandas as pd
+import torch
+import torchaudio
+from tqdm import tqdm
+
+import nkululeko.glob_conf as glob_conf
+from nkululeko.feat_extract.featureset import Featureset
+
+
+class TransformerFeatureExtractor(Featureset):
+    def __init__(self, name, data_df, feat_type):
+        super().__init__(name, data_df, feat_type)
+        cuda = "cuda" if torch.cuda.is_available() else "cpu"
+        self.device = self.util.config_val("MODEL", "device", cuda)
+        self.model_initialized = False
+        self.feat_type = feat_type
+
+    def init_model(self):
+        raise NotImplementedError("Subclasses must implement init_model method")
+
+    def get_embeddings(self, signal, sampling_rate, file):
+        try:
+            with torch.no_grad():
+                # Preprocess the input
+                inputs = self.preprocess_input(signal, sampling_rate)
+
+                # Get model outputs
+                outputs = self.model(**inputs)
+
+                # Extract the relevant hidden states
+                hidden_states = self.extract_hidden_states(outputs)
+
+                # Pool the hidden states
+                embeddings = self.pool_hidden_states(hidden_states)
+
+                # Convert to numpy and flatten
+                embeddings = embeddings.cpu().numpy().ravel()
+
+            return embeddings
+
+        except Exception as e:
+            self.util.error(f"Error extracting embeddings for file {file}: {str(e)}")
+            return np.zeros(self.get_embedding_dim())  # Return zero vector on error
+
+    def preprocess_input(self, signal, sampling_rate):
+        # This method should be implemented by subclasses
+        raise NotImplementedError("Subclasses must implement preprocess_input method")
+
+    def extract_hidden_states(self, outputs):
+        # This method should be implemented by subclasses
+        raise NotImplementedError(
+            "Subclasses must implement extract_hidden_states method"
+        )
+
+    def pool_hidden_states(self, hidden_states):
+        # Default implementation: mean pooling over time dimension
+        return torch.mean(hidden_states, dim=1)
+
+    def get_embedding_dim(self):
+        # This method should be implemented by subclasses
+        raise NotImplementedError("Subclasses must implement get_embedding_dim method")
+
+    def extract(self):
+        store = self.util.get_path("store")
+        storage = f"{store}{self.name}.pkl"
+        extract = self.util.config_val("FEATS", "needs_feature_extraction", False)
+        no_reuse = eval(self.util.config_val("FEATS", "no_reuse", "False"))
+        if extract or no_reuse or not os.path.isfile(storage):
+            if not self.model_initialized:
+                self.init_model()
+            self.util.debug(
+                f"extracting {self.feat_type} embeddings, this might take a while..."
+            )
+            emb_series = pd.Series(index=self.data_df.index, dtype=object)
+            for idx, (file, start, end) in enumerate(
+                tqdm(self.data_df.index.to_list())
+            ):
+                signal, sampling_rate = torchaudio.load(
+                    file,
+                    frame_offset=int(start.total_seconds() * 16000),
+                    num_frames=int((end - start).total_seconds() * 16000),
+                )
+                assert sampling_rate == 16000, f"got {sampling_rate} instead of 16000"
+                emb = self.get_embeddings(signal, sampling_rate, file)
+                emb_series[idx] = emb
+            self.df = pd.DataFrame(emb_series.values.tolist(), index=self.data_df.index)
+            self.df.to_pickle(storage)
+            try:
+                glob_conf.config["DATA"]["needs_feature_extraction"] = "false"
+            except KeyError:
+                pass
+        else:
+            self.util.debug(f"reusing extracted {self.feat_type} embeddings")
+            self.df = pd.read_pickle(storage)
+            if self.df.isnull().values.any():
+                self.util.error(
+                    f"got nan: {self.df.shape} {self.df.isnull().sum().sum()}"
+                )
+
+    def extract_sample(self, signal, sr):
+        self.init_model()
+        feats = self.get_embeddings(signal, sr, "no file")
+        return feats
+
+        # for each feature extractor
+        # feats_ast.py
+
+        # class Ast(TransformerFeatureExtractor):
+        #     def preprocess_input(self, signal, sampling_rate):
+        #         inputs = self.processor(signal.numpy(), sampling_rate=sampling_rate, return_tensors="pt")
+        #         return {k: v.to(self.device) for k, v in inputs.items()}
+
+        #     def extract_hidden_states(self, outputs):
+        #         return outputs.last_hidden_state
+
+        #     def get_embedding_dim(self):
+        #         return self.model.config.hidden_size
+
+        # # feats_wav2vec2.py
+
+        # class Wav2vec2(TransformerFeatureExtractor):
+        #     def preprocess_input(self, signal, sampling_rate):
+        #         inputs = self.processor(signal, sampling_rate=sampling_rate, return_tensors="pt")
+        #         return {k: v.to(self.device) for k, v in inputs.items()}
+
+        #     def extract_hidden_states(self, outputs):
+        #         return outputs.last_hidden_state
+
+        #     def get_embedding_dim(self):
+        #         return self.model.config.hidden_size
+
+        # # feats_wavlm.py
+
+        # class Wavlm(TransformerFeatureExtractor):
+        #     def preprocess_input(self, signal, sampling_rate):
+        #         inputs = self.processor(signal, sampling_rate=sampling_rate, return_tensors="pt")
+        #         return {k: v.to(self.device) for k, v in inputs.items()}
+
+        #     def extract_hidden_states(self, outputs):
+        return outputs.last_hidden_state
+
+    # def get_embedding_dim(self):
+    #     return self.model.config.hidden_size
diff --git a/nkululeko/file_checker.py b/nkululeko/file_checker.py
index 8f6ae663..9c5b144a 100644
--- a/nkululeko/file_checker.py
+++ b/nkululeko/file_checker.py
@@ -1,7 +1,7 @@
-import pandas as pd
-from nkululeko.utils.util import Util
 import os
 
+from nkululeko.utils.util import Util
+
 
 class FileChecker:
     def __init__(self, df):
@@ -10,7 +10,7 @@ def __init__(self, df):
         self.util.copy_flags(df, self.df)
         check_vad = self.util.config_val("DATA", "check_vad", False)
         if check_vad:
-            self.util.debug(f"This may take a while downloading the VAD model")
+            self.util.debug("This may take a while downloading the VAD model")
             import torch
 
             torch.set_num_threads(1)
diff --git a/nkululeko/filter_data.py b/nkululeko/filter_data.py
index 899989e1..58ea3745 100644
--- a/nkululeko/filter_data.py
+++ b/nkululeko/filter_data.py
@@ -1,8 +1,10 @@
+import ast
+
 import audformat
 import pandas as pd
+
 import nkululeko.glob_conf as glob_conf
 from nkululeko.utils.util import Util
-import ast
 
 
 class DataFilter:
diff --git a/nkululeko/fixedsegment.py b/nkululeko/fixedsegment.py
new file mode 100644
index 00000000..7f957c63
--- /dev/null
+++ b/nkululeko/fixedsegment.py
@@ -0,0 +1,83 @@
+"""
+Segment audio files in a given directory into smaller chunks with a specified length and overlap. This requires the PyDub library to be installed.
+
+Args:
+    input_dir (str): The directory containing the audio files to be segmented.
+    output_dir (str): The directory where the segmented audio files will be saved.
+    segment_length (int): The length of each audio segment in milliseconds.
+    overlap (int): The overlap between adjacent audio segments in milliseconds.
+
+This function will recursively search the input directory for all .wav audio files, and then segment each file into smaller chunks with the specified length and overlap. The segmented audio files will be saved in the output directory, preserving the relative directory structure from the input directory.
+"""
+
+import argparse
+import glob
+from pathlib import Path
+
+from pydub import AudioSegment
+
+
+# list audio files given a directory
+def segment_audio(input_dir, output_dir, segment_length, overlap):
+    # check if input dir exist
+    if not Path(input_dir).exists():
+        print(f"Directory {input_dir} does not exist.")
+        return
+
+    # check if output dir exist, create if not
+    if not Path(output_dir).exists():
+        Path(output_dir).mkdir(parents=True)
+
+    audio_files = glob.glob(str(Path(input_dir) / "**" / "*.wav"), recursive=True)
+
+    for audio_file in audio_files:
+        # segment into 2 seconds with 1 second overlap (default values)
+        audio = AudioSegment.from_file(audio_file)
+
+        segments = []
+
+        for i in range(0, len(audio), segment_length - overlap):
+            segment = audio[i : i + segment_length]
+            segments.append(segment)
+
+        # Path(output_dir).mkdir(exist_ok=True)
+        for i, segment in enumerate(segments):
+            # get relative path from input_dir
+            relative_path = Path(audio_file).relative_to(input_dir)
+            # make output directory if not exist
+            output_subdir = Path(output_dir) / relative_path.parent
+            output_subdir.mkdir(parents=True, exist_ok=True)
+            segment.export(
+                str(output_subdir / f"{Path(audio_file).stem}_{i}.wav"),
+                format="wav",
+            )
+
+    print("DONE.")
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--input_dir", type=str, default="./")
+    # add argument for output_dir
+    parser.add_argument(
+        "--output_dir",
+        type=str,
+        default="./segmented_data/",
+    )
+    parser.add_argument(
+        "--segment_length",
+        type=int,
+        default=2000,
+    )
+    parser.add_argument(
+        "--overlap",
+        type=int,
+        default=1000,
+    )
+    args = parser.parse_args()
+    segment_audio(
+        args.input_dir,
+        args.output_dir,
+        segment_length=args.segment_length,
+        overlap=args.overlap,
+    )
diff --git a/nkululeko/models/model.py b/nkululeko/models/model.py
index 438a4efe..57b93bb1 100644
--- a/nkululeko/models/model.py
+++ b/nkululeko/models/model.py
@@ -3,13 +3,11 @@
 import pickle
 import random
 
-from joblib import parallel_backend
 import numpy as np
 import pandas as pd
-from sklearn.model_selection import GridSearchCV
-from sklearn.model_selection import LeaveOneGroupOut
-from sklearn.model_selection import StratifiedKFold
 import sklearn.utils
+from joblib import parallel_backend
+from sklearn.model_selection import GridSearchCV, LeaveOneGroupOut, StratifiedKFold
 
 import nkululeko.glob_conf as glob_conf
 from nkululeko.reporting.reporter import Reporter
@@ -119,7 +117,7 @@ def _do_logo(self):
         # get unique list of speakers
         speakers = annos["speaker"].unique()
         # check for folds columns
-        if not "fold" in annos.columns:
+        if "fold" not in annos.columns:
             self.util.debug(f"creating random folds for {logo} groups")
             # create a random dictionary of groups
             sdict = {}
diff --git a/nkululeko/models/model_bayes.py b/nkululeko/models/model_bayes.py
index dbddbb8b..a3ae97b5 100644
--- a/nkululeko/models/model_bayes.py
+++ b/nkululeko/models/model_bayes.py
@@ -1,6 +1,7 @@
 # model_bayes.py
 
 from sklearn.naive_bayes import GaussianNB
+
 from nkululeko.models.model import Model
 
 
diff --git a/nkululeko/models/model_cnn.py b/nkululeko/models/model_cnn.py
index 883c5a2e..585aac8d 100644
--- a/nkululeko/models/model_cnn.py
+++ b/nkululeko/models/model_cnn.py
@@ -6,23 +6,21 @@
 """
 
 import ast
-from collections import OrderedDict
 
 import numpy as np
 import pandas as pd
-from PIL import Image
-from sklearn.metrics import recall_score
 import torch
 import torch.nn as nn
 import torch.nn.functional as F
-from torch.utils.data import Dataset
 import torchvision.transforms as transforms
+from PIL import Image
+from sklearn.metrics import recall_score
+from torch.utils.data import Dataset
 
 import nkululeko.glob_conf as glob_conf
 from nkululeko.losses.loss_softf1loss import SoftF1Loss
 from nkululeko.models.model import Model
 from nkululeko.reporting.reporter import Reporter
-from nkululeko.utils.util import Util
 
 
 class CNNModel(Model):
@@ -55,7 +53,7 @@ def __init__(self, df_train, df_test, feats_train, feats_test):
             )
         else:
             self.util.error(f"unknown loss function: {criterion}")
-        self.util.debug(f"using model with cross entropy loss function")
+        self.util.debug("using model with cross entropy loss function")
         # set up the model
         # cuda = "cuda" if torch.cuda.is_available() else "cpu"
         self.device = self.util.config_val("MODEL", "device", "cpu")
diff --git a/nkululeko/models/model_gmm.py b/nkululeko/models/model_gmm.py
index b561437e..e74e738e 100644
--- a/nkululeko/models/model_gmm.py
+++ b/nkululeko/models/model_gmm.py
@@ -1,8 +1,10 @@
 # model_gmm.py
 
+import pandas as pd
 from sklearn import mixture
+
 from nkululeko.models.model import Model
-import pandas as pd
+
 
 class GMM_model(Model):
     """An GMM model"""
@@ -15,23 +17,25 @@ def __init__(self, df_train, df_test, feats_train, feats_test):
         self.n_components = int(self.util.config_val("MODEL", "GMM_components", "4"))
         covariance_type = self.util.config_val("MODEL", "GMM_covariance_type", "full")
         self.clf = mixture.GaussianMixture(
-            n_components=self.n_components, 
+            n_components=self.n_components,
             covariance_type=covariance_type,
-            random_state = 42,
+            random_state=42,
         )
         # set up the classifier
 
     def get_predictions(self):
-        """Use the predict_proba method of the GaussianMixture model to get 
-        probabilities. Create a DataFrame with these probabilities and return 
+        """Use the predict_proba method of the GaussianMixture model to get
+        probabilities. Create a DataFrame with these probabilities and return
         it along with the predictions."""
         probs = self.clf.predict_proba(self.feats_test)
         preds = self.clf.predict(self.feats_test)
-        
+
         # Convert predictions to a list
         preds = preds.tolist()
-        
+
         # Create a DataFrame for probabilities
-        proba_df = pd.DataFrame(probs, index=self.feats_test.index, columns=range(self.n_components))
-        
+        proba_df = pd.DataFrame(
+            probs, index=self.feats_test.index, columns=range(self.n_components)
+        )
+
         return preds, proba_df
diff --git a/nkululeko/models/model_knn.py b/nkululeko/models/model_knn.py
index 4c77fbbe..12b7dd8c 100644
--- a/nkululeko/models/model_knn.py
+++ b/nkululeko/models/model_knn.py
@@ -1,6 +1,7 @@
 # model_knn.py
 
 from sklearn.neighbors import KNeighborsClassifier
+
 from nkululeko.models.model import Model
 
 
diff --git a/nkululeko/models/model_knn_reg.py b/nkululeko/models/model_knn_reg.py
index b728679f..029a6e50 100644
--- a/nkululeko/models/model_knn_reg.py
+++ b/nkululeko/models/model_knn_reg.py
@@ -1,6 +1,7 @@
 # model_knn_reg.py
 
 from sklearn.neighbors import KNeighborsRegressor
+
 from nkululeko.models.model import Model
 
 
diff --git a/nkululeko/models/model_lin_reg.py b/nkululeko/models/model_lin_reg.py
index dc5b7491..0189463d 100644
--- a/nkululeko/models/model_lin_reg.py
+++ b/nkululeko/models/model_lin_reg.py
@@ -1,6 +1,7 @@
 # model_knn_reg.py
 
 from sklearn.linear_model import LinearRegression
+
 from nkululeko.models.model import Model
 
 
diff --git a/nkululeko/models/model_mlp.py b/nkululeko/models/model_mlp.py
index 3fcffec0..8b2bece5 100644
--- a/nkululeko/models/model_mlp.py
+++ b/nkululeko/models/model_mlp.py
@@ -4,14 +4,13 @@
 
 import numpy as np
 import pandas as pd
-from sklearn.metrics import recall_score
 import torch
+from sklearn.metrics import recall_score
 
 import nkululeko.glob_conf as glob_conf
 from nkululeko.losses.loss_softf1loss import SoftF1Loss
 from nkululeko.models.model import Model
 from nkululeko.reporting.reporter import Reporter
-from nkululeko.utils.util import Util
 
 
 class MLPModel(Model):
@@ -44,7 +43,7 @@ def __init__(self, df_train, df_test, feats_train, feats_test):
             )
         else:
             self.util.error(f"unknown loss function: {criterion}")
-        self.util.debug(f"using model with cross entropy loss function")
+        self.util.debug("using model with cross entropy loss function")
         # set up the model, use GPU if availabe
         cuda = "cuda" if torch.cuda.is_available() else "cpu"
         self.device = self.util.config_val("MODEL", "device", cuda)
diff --git a/nkululeko/models/model_mlp_regression.py b/nkululeko/models/model_mlp_regression.py
index b3f6e1a5..36bb4667 100644
--- a/nkululeko/models/model_mlp_regression.py
+++ b/nkululeko/models/model_mlp_regression.py
@@ -1,15 +1,10 @@
 # model_mlp.py
 import ast
 from collections import OrderedDict
-import os
 
 import numpy as np
 import torch
-
-from audmetric import concordance_cc
-from audmetric import mean_absolute_error
-from audmetric import mean_squared_error
-from traitlets import default
+from audmetric import concordance_cc, mean_absolute_error, mean_squared_error
 
 import nkululeko.glob_conf as glob_conf
 from nkululeko.losses.loss_ccc import ConcordanceCorCoeff
diff --git a/nkululeko/models/model_svm.py b/nkululeko/models/model_svm.py
index 5f3705b6..f7f9c51a 100644
--- a/nkululeko/models/model_svm.py
+++ b/nkululeko/models/model_svm.py
@@ -1,7 +1,7 @@
 # model_svm.py
 
-import random
 from sklearn import svm
+
 from nkululeko.models.model import Model
 
 
@@ -25,7 +25,7 @@ def __init__(self, df_train, df_test, feats_train, feats_test):
             gamma="scale",
             probability=True,
             class_weight=class_weight,
-            random_state=42,    # for consistent result
+            random_state=42,  # for consistent result
         )  # set up the classifier
 
     def set_c(self, c):
diff --git a/nkululeko/models/model_svr.py b/nkululeko/models/model_svr.py
index ee6d4240..e7cdcd7e 100644
--- a/nkululeko/models/model_svr.py
+++ b/nkululeko/models/model_svr.py
@@ -1,6 +1,7 @@
 # svrmodel.py
 
 from sklearn import svm
+
 from nkululeko.models.model import Model
 
 
diff --git a/nkululeko/models/model_tree.py b/nkululeko/models/model_tree.py
index 8a37af0e..5d5a5886 100644
--- a/nkululeko/models/model_tree.py
+++ b/nkululeko/models/model_tree.py
@@ -1,6 +1,7 @@
 # model_tree.py
 
 from sklearn.tree import DecisionTreeClassifier
+
 from nkululeko.models.model import Model
 
 
@@ -12,6 +13,4 @@ class Tree_model(Model):
     def __init__(self, df_train, df_test, feats_train, feats_test):
         super().__init__(df_train, df_test, feats_train, feats_test)
         self.name = "tree"
-        self.clf = DecisionTreeClassifier(
-            random_state=42
-        )  # set up the classifier
+        self.clf = DecisionTreeClassifier(random_state=42)  # set up the classifier
diff --git a/nkululeko/models/model_tree_reg.py b/nkululeko/models/model_tree_reg.py
index f5ad2309..7070790b 100644
--- a/nkululeko/models/model_tree_reg.py
+++ b/nkululeko/models/model_tree_reg.py
@@ -1,6 +1,7 @@
 # model_tree_reg.py
 
 from sklearn.tree import DecisionTreeRegressor
+
 from nkululeko.models.model import Model
 
 
diff --git a/nkululeko/models/model_tuned.py b/nkululeko/models/model_tuned.py
index 99b0b8c2..40d45bec 100644
--- a/nkululeko/models/model_tuned.py
+++ b/nkululeko/models/model_tuned.py
@@ -7,17 +7,18 @@
 import pickle
 import typing
 
+import audeer
+import audiofile
+import audmetric
 import datasets
 import numpy as np
 import pandas as pd
 import torch
 import transformers
-from transformers.models.wav2vec2.modeling_wav2vec2 import Wav2Vec2Model
-from transformers.models.wav2vec2.modeling_wav2vec2 import Wav2Vec2PreTrainedModel
-
-import audeer
-import audiofile
-import audmetric
+from transformers.models.wav2vec2.modeling_wav2vec2 import (
+    Wav2Vec2Model,
+    Wav2Vec2PreTrainedModel,
+)
 
 import nkululeko.glob_conf as glob_conf
 from nkululeko.models.model import Model as BaseModel
@@ -25,7 +26,6 @@
 
 
 class TunedModel(BaseModel):
-
     def __init__(self, df_train, df_test, feats_train, feats_test):
         """Constructor taking the configuration and all dataframes."""
         super().__init__(df_train, df_test, feats_train, feats_test)
@@ -54,8 +54,7 @@ def __init__(self, df_train, df_test, feats_train, feats_test):
         self.learning_rate = float(
             self.util.config_val("MODEL", "learning_rate", "0.0001")
         )
-        self.max_duration = float(
-            self.util.config_val("MODEL", "max_duration", "8.0"))
+        self.max_duration = float(self.util.config_val("MODEL", "max_duration", "8.0"))
         self.df_train, self.df_test = df_train, df_test
         self.epoch_num = int(self.util.config_val("EXP", "epochs", 1))
         self.util.debug(f"num of epochs: {self.epoch_num}")
@@ -65,12 +64,12 @@ def __init__(self, df_train, df_test, feats_train, feats_test):
             self.drop = float(drop)
         self.util.debug(f"init: training with dropout: {self.drop}")
         self.push = eval(self.util.config_val("MODEL", "push_to_hub", "False"))
+        self.balancing = self.util.config_val("MODEL", "balancing", False)
         self._init_model()
 
     def _init_model(self):
         model_path = "facebook/wav2vec2-large-robust-ft-swbd-300h"
-        pretrained_model = self.util.config_val(
-            "MODEL", "pretrained_model", model_path)
+        pretrained_model = self.util.config_val("MODEL", "pretrained_model", model_path)
         self.num_layers = None
         self.sampling_rate = 16000
         self.max_duration_sec = self.max_duration
@@ -94,6 +93,42 @@ def _init_model(self):
             df = y.reset_index()
             df.start = df.start.dt.total_seconds()
             df.end = df.end.dt.total_seconds()
+            #     ds = datasets.Dataset.from_pandas(df)
+            #     dataset[split] = ds
+
+            # self.dataset = datasets.DatasetDict(dataset)
+            if split == "train" and self.balancing:
+                if self.balancing == "ros":
+                    from imblearn.over_sampling import RandomOverSampler
+
+                    sampler = RandomOverSampler(random_state=42)
+                elif self.balancing == "smote":
+                    from imblearn.over_sampling import SMOTE
+
+                    sampler = SMOTE(random_state=42)
+                elif self.balancing == "adasyn":
+                    from imblearn.over_sampling import ADASYN
+
+                    sampler = ADASYN(random_state=42)
+                else:
+                    self.util.error(f"Unknown balancing algorithm: {self.balancing}")
+
+                X_resampled, y_resampled = sampler.fit_resample(
+                    df[["start", "end"]], df["targets"]
+                )
+                df = pd.DataFrame(
+                    {
+                        "start": X_resampled["start"],
+                        "end": X_resampled["end"],
+                        "targets": y_resampled,
+                    }
+                )
+
+                # print the before and after class distribution
+                self.util.debug(
+                    f"balanced with: {self.balancing}, new size: {len(df)}, was {len(data_sources[split])}"
+                )
+
             ds = datasets.Dataset.from_pandas(df)
             dataset[split] = ds
 
@@ -101,7 +136,7 @@ def _init_model(self):
 
         # load pre-trained model
         if self.is_classifier:
-            self.util.debug(f"Task is classification.")
+            self.util.debug("Task is classification.")
             le = glob_conf.label_encoder
             mapping = dict(zip(le.classes_, range(len(le.classes_))))
             target_mapping = {k: int(v) for k, v in mapping.items()}
@@ -116,7 +151,7 @@ def _init_model(self):
                 finetuning_task=target_name,
             )
         else:
-            self.util.debug(f"Task is regression.")
+            self.util.debug("Task is regression.")
             self.config = transformers.AutoConfig.from_pretrained(
                 pretrained_model,
                 num_labels=1,
@@ -215,7 +250,6 @@ def data_collator(self, data):
         return batch
 
     def compute_metrics(self, p: transformers.EvalPrediction):
-
         metrics = {
             "UAR": audmetric.unweighted_average_recall,
             "ACC": audmetric.accuracy,
@@ -268,8 +302,7 @@ def train(self):
                 else:
                     criterion = torch.nn.CrossEntropyLoss()
             else:
-                self.util.error(
-                    f"criterion {criterion} not supported for classifier")
+                self.util.error(f"criterion {criterion} not supported for classifier")
         else:
             self.criterion = self.util.config_val("MODEL", "loss", "ccc")
             if criterion == "1-ccc":
@@ -279,8 +312,7 @@ def train(self):
             elif criterion == "mae":
                 criterion = torch.nn.L1Loss()
             else:
-                self.util.error(
-                    f"criterion {criterion} not supported for regressor")
+                self.util.error(f"criterion {criterion} not supported for regressor")
 
         # set push_to_hub value, default false
         # push = eval(self.util.config_val("MODEL", "push_to_hub", "False"))
@@ -319,8 +351,7 @@ def compute_loss(
         elif metrics_for_best_model == "MAE":
             greater_is_better = False
         else:
-            self.util.error(
-                f"unknown metric/measure: {metrics_for_best_model}")
+            self.util.error(f"unknown metric/measure: {metrics_for_best_model}")
 
         training_args = transformers.TrainingArguments(
             output_dir=model_root,
@@ -453,17 +484,15 @@ def load_path(self, path, run, epoch):
             self.clf = pickle.load(handle)
 
 
-@ dataclasses.dataclass
+@dataclasses.dataclass
 class ModelOutput(transformers.file_utils.ModelOutput):
-
     logits: torch.FloatTensor = None
     hidden_states: typing.Tuple[torch.FloatTensor] = None
     cnn_features: torch.FloatTensor = None
 
 
-@ dataclasses.dataclass
+@dataclasses.dataclass
 class ModelOutputReg(transformers.file_utils.ModelOutput):
-
     logits: torch.FloatTensor
     hidden_states: typing.Tuple[torch.FloatTensor] = None
     attentions: typing.Tuple[torch.FloatTensor] = None
@@ -473,9 +502,7 @@ class ModelOutputReg(transformers.file_utils.ModelOutput):
 
 
 class ModelHead(torch.nn.Module):
-
     def __init__(self, config):
-
         super().__init__()
 
         self.dense = torch.nn.Linear(config.hidden_size, config.hidden_size)
@@ -483,7 +510,6 @@ def __init__(self, config):
         self.out_proj = torch.nn.Linear(config.hidden_size, config.num_labels)
 
     def forward(self, features, **kwargs):
-
         x = features
         x = self.dropout(x)
         x = self.dense(x)
@@ -495,9 +521,7 @@ def forward(self, features, **kwargs):
 
 
 class Model(Wav2Vec2PreTrainedModel):
-
     def __init__(self, config):
-
         if not hasattr(config, "add_adapter"):
             setattr(config, "add_adapter", False)
 
@@ -516,7 +540,6 @@ def pooling(
         hidden_states,
         attention_mask,
     ):
-
         if attention_mask is None:  # For evaluation with batch_size==1
             outputs = torch.mean(hidden_states, dim=1)
         else:
@@ -532,8 +555,7 @@ def pooling(
             attention_sum = torch.sum(attention_mask, dim=1)
 
             epsilon = 1e-6  # to avoid division by zero and numerical instability
-            outputs = outputs / (torch.reshape(attention_sum, (-1, 1)) +
-                                 epsilon)
+            outputs = outputs / (torch.reshape(attention_sum, (-1, 1)) + epsilon)
 
         return outputs
 
@@ -590,7 +612,6 @@ def predict(self, signal):
 
 
 class ConcordanceCorCoeff(torch.nn.Module):
-
     def __init__(self):
         super().__init__()
         self.mean = torch.mean
diff --git a/nkululeko/models/model_xgb.py b/nkululeko/models/model_xgb.py
index 681ec37a..85b3a212 100644
--- a/nkululeko/models/model_xgb.py
+++ b/nkululeko/models/model_xgb.py
@@ -1,6 +1,7 @@
 # xgbmodel.py
 
 from xgboost import XGBClassifier
+
 from nkululeko.models.model import Model
 
 
diff --git a/nkululeko/models/model_xgr.py b/nkululeko/models/model_xgr.py
index f78bfebb..2347f34a 100644
--- a/nkululeko/models/model_xgr.py
+++ b/nkululeko/models/model_xgr.py
@@ -1,6 +1,7 @@
 # xgrmodel.py
 
 from xgboost.sklearn import XGBRegressor
+
 from nkululeko.models.model import Model
 
 
diff --git a/nkululeko/multidb.py b/nkululeko/multidb.py
index d69738f3..a7c9cb6a 100644
--- a/nkululeko/multidb.py
+++ b/nkululeko/multidb.py
@@ -5,6 +5,7 @@
 
 The `plot_heatmap` function generates a heatmap plot of the results and saves it to a file, along with some summary statistics.
 """
+
 # main.py
 # Demonstration code to use the ML-experiment framework
 
diff --git a/nkululeko/nkululeko.py b/nkululeko/nkululeko.py
index d5c6db62..e2b1a137 100644
--- a/nkululeko/nkululeko.py
+++ b/nkululeko/nkululeko.py
@@ -6,8 +6,8 @@
 
 import numpy as np
 
-from nkululeko.constants import VERSION
 import nkululeko.experiment as exp
+from nkululeko.constants import VERSION
 from nkululeko.utils.util import Util
 
 
diff --git a/nkululeko/predict.py b/nkululeko/predict.py
index 7d830de5..d4c5c6ec 100644
--- a/nkululeko/predict.py
+++ b/nkululeko/predict.py
@@ -26,9 +26,9 @@
 
 def main(src_dir):
     parser = argparse.ArgumentParser(
-        description="Call the nkululeko PREDICT framework.")
-    parser.add_argument("--config", default="exp.ini",
-                        help="The base configuration")
+        description="Call the nkululeko PREDICT framework."
+    )
+    parser.add_argument("--config", default="exp.ini", help="The base configuration")
     args = parser.parse_args()
     if args.config is not None:
         config_file = args.config
@@ -58,8 +58,7 @@ def main(src_dir):
 
     # split into train and test
     expr.fill_train_and_tests()
-    util.debug(
-        f"train shape : {expr.df_train.shape}, test shape:{expr.df_test.shape}")
+    util.debug(f"train shape : {expr.df_train.shape}, test shape:{expr.df_test.shape}")
 
     # process the data
     df = expr.autopredict()
diff --git a/nkululeko/reporting/defines.py b/nkululeko/reporting/defines.py
index bcbe871b..c385148b 100644
--- a/nkululeko/reporting/defines.py
+++ b/nkululeko/reporting/defines.py
@@ -1,5 +1,4 @@
 class DefineBase:
-
     @classmethod
     def _assert_has_attribute_value(cls, value):
         valid_values = cls._attribute_values()
@@ -8,17 +7,16 @@ def _assert_has_attribute_value(cls, value):
 
     @classmethod
     def _attribute_values(cls):
-        attributes = inspect.getmembers(
-            cls, lambda x: not inspect.isroutine(x)
-        )
+        attributes = inspect.getmembers(cls, lambda x: not inspect.isroutine(x))
         return sorted(
             [
-                a[1] for a in attributes
-                if not (a[0].startswith('__') and a[0].endswith('__'))
+                a[1]
+                for a in attributes
+                if not (a[0].startswith("__") and a[0].endswith("__"))
             ]
         )
 
 
 class Header(DefineBase):
-    HEADER_RESULTS = 'Results'
-    HEADER_EXPLORE = 'Data exploration'
+    HEADER_RESULTS = "Results"
+    HEADER_EXPLORE = "Data exploration"
diff --git a/nkululeko/reporting/latex_writer.py b/nkululeko/reporting/latex_writer.py
index f389483e..34611a15 100644
--- a/nkululeko/reporting/latex_writer.py
+++ b/nkululeko/reporting/latex_writer.py
@@ -3,9 +3,9 @@
 print out report as latex file and pdf
 """
 
-from pylatex import Document, Section, Subsection, Command, Figure
-from pylatex.utils import italic, NoEscape
-from nkululeko.reporting.report_item import ReportItem
+from pylatex import Command, Document, Figure, Section, Subsection
+from pylatex.utils import NoEscape
+
 from nkululeko.utils.util import Util
 
 
diff --git a/nkululeko/reporting/report.py b/nkululeko/reporting/report.py
index 87f1d121..d666cf76 100644
--- a/nkululeko/reporting/report.py
+++ b/nkululeko/reporting/report.py
@@ -4,9 +4,9 @@
 Collector class for report items collected during module processing.
 
 """
-from nkululeko.utils.util import Util
-from nkululeko.reporting.report_item import ReportItem
+
 from nkululeko.reporting.latex_writer import LatexWriter
+from nkululeko.utils.util import Util
 
 
 class Report:
diff --git a/nkululeko/reporting/report_item.py b/nkululeko/reporting/report_item.py
index a03409df..c91959d0 100644
--- a/nkululeko/reporting/report_item.py
+++ b/nkululeko/reporting/report_item.py
@@ -4,6 +4,7 @@
 
 import os.path
 
+
 class ReportItem:
     def __init__(self, topic, caption, contents, image=None):
         self.topic = topic
diff --git a/nkululeko/reporting/reporter.py b/nkululeko/reporting/reporter.py
index c501648f..ad45c78a 100644
--- a/nkululeko/reporting/reporter.py
+++ b/nkululeko/reporting/reporter.py
@@ -21,13 +21,13 @@
 from scipy.stats import entropy, pearsonr
 from sklearn.metrics import (
     ConfusionMatrixDisplay,
+    RocCurveDisplay,
     auc,
     classification_report,
     confusion_matrix,
     r2_score,
     roc_auc_score,
     roc_curve,
-    RocCurveDisplay,
 )
 
 import nkululeko.glob_conf as glob_conf
@@ -419,24 +419,25 @@ def print_results(self, epoch=None):
                 f1_per_class = (
                     f"result per class (F1 score): {c_ress} from epoch: {epoch}"
                 )
-                if len(np.unique(self.truths)) == 2:
-                    fpr, tpr, _ = roc_curve(self.truths, self.preds)
-                    auc_score = auc(fpr, tpr)
-                    plot_path = f"{fig_dir}{self.util.get_exp_name()}_{epoch}{self.filenameadd}_roc.{self.format}"
-                    plt.figure()
-                    display = RocCurveDisplay(
-                        fpr=fpr,
-                        tpr=tpr,
-                        roc_auc=auc_score,
-                        estimator_name=f"{self.model_type} estimator",
-                    )
-                    display.plot(ax=None)
-                    plt.savefig(plot_path)
-                    plt.close()
-                    self.util.debug(f"Saved ROC curve to {plot_path}")
-                    pauc_score = roc_auc_score(self.truths, self.preds, max_fpr=0.1)
-                    auc_pauc = f"auc: {auc_score:.3f}, pauc: {pauc_score:.3f} from epoch: {epoch}"
-                    self.util.debug(auc_pauc)
+                # the following auc is buggy, preds should be probabilities
+                # if len(np.unique(self.truths)) == 2:
+                #     fpr, tpr, _ = roc_curve(self.truths, self.preds)
+                #     auc_score = auc(fpr, tpr)
+                #     plot_path = f"{fig_dir}{self.util.get_exp_name()}_{epoch}{self.filenameadd}_roc.{self.format}"
+                #     plt.figure()
+                #     display = RocCurveDisplay(
+                #         fpr=fpr,
+                #         tpr=tpr,
+                #         roc_auc=auc_score,
+                #         estimator_name=f"{self.model_type} estimator",
+                #     )
+                #     display.plot(ax=None)
+                #     plt.savefig(plot_path)
+                #     plt.close()
+                #     self.util.debug(f"Saved ROC curve to {plot_path}")
+                #     pauc_score = roc_auc_score(self.truths, self.preds, max_fpr=0.1)
+                #     auc_pauc = f"auc: {auc_score:.3f}, pauc: {pauc_score:.3f} from epoch: {epoch}"
+                #     self.util.debug(auc_pauc)
                 self.util.debug(f1_per_class)
                 rpt_str = f"{json.dumps(rpt)}\n{f1_per_class}"
                 # rpt_str += f"\n{auc_auc}"
diff --git a/nkululeko/resample.py b/nkululeko/resample.py
index e5ad1e84..bf9476a7 100644
--- a/nkululeko/resample.py
+++ b/nkululeko/resample.py
@@ -4,24 +4,23 @@
 import argparse
 import configparser
 import os
-import pandas as pd
+
 import audformat
-from nkululeko.augmenting.resampler import Resampler
-from nkululeko.utils.util import Util
+import pandas as pd
 
+from nkululeko.augmenting.resampler import Resampler
 from nkululeko.constants import VERSION
 from nkululeko.experiment import Experiment
 from nkululeko.utils.files import find_files
+from nkululeko.utils.util import Util
 
 
 def main(src_dir):
     parser = argparse.ArgumentParser(
         description="Call the nkululeko RESAMPLE framework."
     )
-    parser.add_argument("--config", default=None,
-                        help="The base configuration")
-    parser.add_argument("--file", default=None,
-                        help="The input audio file to resample")
+    parser.add_argument("--config", default=None, help="The base configuration")
+    parser.add_argument("--file", default=None, help="The input audio file to resample")
     parser.add_argument(
         "--folder",
         default=None,
@@ -34,9 +33,7 @@ def main(src_dir):
     args = parser.parse_args()
 
     if args.file is None and args.folder is None and args.config is None:
-        print(
-            "ERROR: Either --file, --folder, or --config argument must be provided."
-        )
+        print("ERROR: Either --file, --folder, or --config argument must be provided.")
         exit()
 
     if args.file is not None:
@@ -102,8 +99,7 @@ def main(src_dir):
             f"train shape : {expr.df_train.shape}, test shape:{expr.df_test.shape}"
         )
 
-        sample_selection = util.config_val(
-            "RESAMPLE", "sample_selection", "all")
+        sample_selection = util.config_val("RESAMPLE", "sample_selection", "all")
         if sample_selection == "all":
             df = pd.concat([expr.df_train, expr.df_test])
         elif sample_selection == "train":
diff --git a/nkululeko/resample_cli.py b/nkululeko/resample_cli.py
new file mode 100644
index 00000000..c0ebfec0
--- /dev/null
+++ b/nkululeko/resample_cli.py
@@ -0,0 +1,99 @@
+import argparse
+import configparser
+import os
+
+import audformat
+import pandas as pd
+
+from nkululeko.augmenting.resampler import Resampler
+from nkululeko.constants import VERSION
+from nkululeko.experiment import Experiment
+from nkululeko.utils.util import Util
+
+
+def main(src_dir):
+    parser = argparse.ArgumentParser(
+        description="Call the nkululeko RESAMPLE framework."
+    )
+    parser.add_argument("--config", default=None, help="The base configuration")
+    parser.add_argument("--file", default=None, help="The input audio file to resample")
+    parser.add_argument(
+        "--replace", action="store_true", help="Replace the original audio file"
+    )
+
+    args = parser.parse_args()
+
+    if args.file is None and args.config is None:
+        print("ERROR: Either --file or --config argument must be provided.")
+        exit()
+
+    if args.file is not None:
+        # Load the audio file into a DataFrame
+        files = pd.Series([args.file])
+        df_sample = pd.DataFrame(index=files)
+        df_sample.index = audformat.utils.to_segmented_index(
+            df_sample.index, allow_nat=False
+        )
+
+        # Resample the audio file
+        util = Util("resampler", has_config=False)
+        util.debug(f"Resampling audio file: {args.file}")
+        rs = Resampler(df_sample, not_testing=True, replace=args.replace)
+        rs.resample()
+    else:
+        # Existing code for handling INI file
+        config_file = args.config
+
+        # Test if the configuration file exists
+        if not os.path.isfile(config_file):
+            print(f"ERROR: no such file: {config_file}")
+            exit()
+
+        # Load one configuration per experiment
+        config = configparser.ConfigParser()
+        config.read(config_file)
+        # Create a new experiment
+        expr = Experiment(config)
+        module = "resample"
+        expr.set_module(module)
+        util = Util(module)
+        util.debug(
+            f"running {expr.name} from config {config_file}, nkululeko version"
+            f" {VERSION}"
+        )
+
+        if util.config_val("EXP", "no_warnings", False):
+            import warnings
+
+            warnings.filterwarnings("ignore")
+
+        # Load the data
+        expr.load_datasets()
+
+        # Split into train and test
+        expr.fill_train_and_tests()
+        util.debug(
+            f"train shape : {expr.df_train.shape}, test shape:{expr.df_test.shape}"
+        )
+
+        sample_selection = util.config_val("RESAMPLE", "sample_selection", "all")
+        if sample_selection == "all":
+            df = pd.concat([expr.df_train, expr.df_test])
+        elif sample_selection == "train":
+            df = expr.df_train
+        elif sample_selection == "test":
+            df = expr.df_test
+        else:
+            util.error(
+                f"unknown selection specifier {sample_selection}, should be [all |"
+                " train | test]"
+            )
+        util.debug(f"resampling {sample_selection}: {df.shape[0]} samples")
+        replace = util.config_val("RESAMPLE", "replace", "False")
+        rs = Resampler(df, replace=replace)
+        rs.resample()
+
+
+if __name__ == "__main__":
+    cwd = os.path.dirname(os.path.abspath(__file__))
+    main(cwd)
diff --git a/nkululeko/runmanager.py b/nkululeko/runmanager.py
index 5e4eee9f..f47780b5 100644
--- a/nkululeko/runmanager.py
+++ b/nkululeko/runmanager.py
@@ -50,7 +50,9 @@ def do_runs(self):
         self.last_epochs = []  # keep the epoch of best result per run
         # for all runs
         for run in range(int(self.util.config_val("EXP", "runs", 1))):
-            self.util.debug(f"run {run} using model {glob_conf.config['MODEL']['type']}")
+            self.util.debug(
+                f"run {run} using model {glob_conf.config['MODEL']['type']}"
+            )
             # set the run index as global variable for reporting
             self.util.set_config_val("EXP", "run", run)
             self.modelrunner = Modelrunner(
diff --git a/nkululeko/scaler.py b/nkululeko/scaler.py
index 260974f0..63389e24 100644
--- a/nkululeko/scaler.py
+++ b/nkululeko/scaler.py
@@ -1,8 +1,8 @@
 # scaler.py
 
 import numpy as np
-from sklearn.preprocessing import StandardScaler, RobustScaler
 import pandas as pd
+from sklearn.preprocessing import RobustScaler, StandardScaler
 
 from nkululeko.utils.util import Util
 
diff --git a/nkululeko/segment.py b/nkululeko/segment.py
index 61a46911..887ae51c 100644
--- a/nkululeko/segment.py
+++ b/nkululeko/segment.py
@@ -2,14 +2,16 @@
 # segment data splits
 
 import argparse
+import configparser
 import os
+
 import pandas as pd
-import configparser
-from nkululeko.experiment import Experiment
-from nkululeko.utils.util import Util
-from nkululeko.constants import VERSION
+
 import nkululeko.glob_conf as glob_conf
+from nkululeko.constants import VERSION
+from nkululeko.experiment import Experiment
 from nkululeko.reporting.report_item import ReportItem
+from nkululeko.utils.util import Util
 
 
 def main(src_dir):
@@ -75,7 +77,6 @@ def main(src_dir):
         util.error(f"unkown segmenter: {segmenter}")
 
     def calc_dur(x):
-        from datetime import datetime
 
         starts = x[1]
         ends = x[2]
diff --git a/nkululeko/segmenting/seg_inaspeechsegmenter.py b/nkululeko/segmenting/seg_inaspeechsegmenter.py
index 57187e8f..8bf8c1b2 100644
--- a/nkululeko/segmenting/seg_inaspeechsegmenter.py
+++ b/nkululeko/segmenting/seg_inaspeechsegmenter.py
@@ -1,11 +1,11 @@
+import warnings
+
 import audformat
 import pandas as pd
+from audformat import segmented_index
 
 # segment the data
 from inaSpeechSegmenter import Segmenter
-from audformat.utils import to_filewise_index
-from audformat import segmented_index
-import warnings
 
 
 class Ina_segmenter:
diff --git a/nkululeko/segmenting/seg_silero.py b/nkululeko/segmenting/seg_silero.py
index 05972a97..00331ae4 100644
--- a/nkululeko/segmenting/seg_silero.py
+++ b/nkululeko/segmenting/seg_silero.py
@@ -5,12 +5,12 @@
 
 """
 
-import torch
-import pandas as pd
-from tqdm import tqdm
 import audformat
-from audformat.utils import to_filewise_index
+import pandas as pd
+import torch
 from audformat import segmented_index
+from tqdm import tqdm
+
 from nkululeko.utils.util import Util
 
 # from nkululeko.constants import SAMPLING_RATE
diff --git a/nkululeko/syllable_nuclei.py b/nkululeko/syllable_nuclei.py
index 5dfc5353..26ee6d5c 100644
--- a/nkululeko/syllable_nuclei.py
+++ b/nkululeko/syllable_nuclei.py
@@ -62,12 +62,12 @@
 
 
 import math
+from glob import glob
+
 import pandas as pd
 import parselmouth
-from tqdm import tqdm
-
-from glob import glob
 from parselmouth.praat import call
+from tqdm import tqdm
 
 
 def speech_rate(filename):
@@ -120,9 +120,7 @@ def speech_rate(filename):
     # use total duration, not end time, to find out duration of intdur (intensity_duration)
     # in order to allow nonzero starting times.
     intensity_duration = call(sound_from_intensity_matrix, "Get total duration")
-    intensity_max = call(
-        sound_from_intensity_matrix, "Get maximum", 0, 0, "Parabolic"
-    )
+    intensity_max = call(sound_from_intensity_matrix, "Get maximum", 0, 0, "Parabolic")
     point_process = call(
         sound_from_intensity_matrix,
         "To PointProcess (extrema)",
@@ -133,19 +131,14 @@ def speech_rate(filename):
     )
     # estimate peak positions (all peaks)
     numpeaks = call(point_process, "Get number of points")
-    t = [
-        call(point_process, "Get time from index", i + 1)
-        for i in range(numpeaks)
-    ]
+    t = [call(point_process, "Get time from index", i + 1) for i in range(numpeaks)]
 
     # fill array with intensity values
     timepeaks = []
     peakcount = 0
     intensities = []
     for i in range(numpeaks):
-        value = call(
-            sound_from_intensity_matrix, "Get value at time", t[i], "Cubic"
-        )
+        value = call(sound_from_intensity_matrix, "Get value at time", t[i], "Cubic")
         if value > threshold:
             peakcount += 1
             intensities.append(value)
@@ -161,22 +154,16 @@ def speech_rate(filename):
     for p in range(peakcount - 1):
         following = p + 1
         followingtime = timepeaks[p + 1]
-        dip = call(
-            intensity, "Get minimum", currenttime, timepeaks[p + 1], "None"
-        )
+        dip = call(intensity, "Get minimum", currenttime, timepeaks[p + 1], "None")
         diffint = abs(currentint - dip)
         if diffint > mindip:
             validpeakcount += 1
             validtime.append(timepeaks[p])
         currenttime = timepeaks[following]
-        currentint = call(
-            intensity, "Get value at time", timepeaks[following], "Cubic"
-        )
+        currentint = call(intensity, "Get value at time", timepeaks[following], "Cubic")
 
     # Look for only voiced parts
-    pitch = sound.to_pitch_ac(
-        0.02, 30, 4, False, 0.03, 0.25, 0.01, 0.35, 0.25, 450
-    )
+    pitch = sound.to_pitch_ac(0.02, 30, 4, False, 0.03, 0.25, 0.01, 0.35, 0.25, 450)
     voicedcount = 0
     voicedpeak = []
 
diff --git a/nkululeko/utils/stats.py b/nkululeko/utils/stats.py
index 1f3d0187..70ed605d 100644
--- a/nkululeko/utils/stats.py
+++ b/nkululeko/utils/stats.py
@@ -2,7 +2,6 @@
 from itertools import combinations
 
 import numpy as np
-import pandas as pd
 
 
 def check_na(a):
diff --git a/nkululeko/utils/util.py b/nkululeko/utils/util.py
index f483c375..aa584a3d 100644
--- a/nkululeko/utils/util.py
+++ b/nkululeko/utils/util.py
@@ -6,11 +6,10 @@
 import pickle
 import sys
 
-import numpy as np
-import pandas as pd
-
 import audeer
 import audformat
+import numpy as np
+import pandas as pd
 
 
 class Util:
diff --git a/setup.py b/setup.py
index 26126d85..d5d43d7c 100644
--- a/setup.py
+++ b/setup.py
@@ -1,3 +1,3 @@
 from setuptools import setup
 
-setup(use_scm_version=True)
\ No newline at end of file
+setup(use_scm_version=True)
diff --git a/tutorials/tut_spotlight_ravdess.ini b/tutorials/tut_spotlight_ravdess.ini
new file mode 100644
index 00000000..e6421451
--- /dev/null
+++ b/tutorials/tut_spotlight_ravdess.ini
@@ -0,0 +1,31 @@
+[EXP]
+root = ./
+name = results/exp_ravdess_wavlm
+runs = 1
+epochs = 1
+save = True
+[DATA]
+databases = ['train', 'test', 'dev']
+train = ./data/ravdess/ravdess_train.csv
+train.type = csv
+train.absolute_path = False
+train.split_strategy = train
+dev = ./data/ravdess/ravdess_dev.csv
+dev.type = csv
+dev.absolute_path = False
+dev.split_strategy = train
+test = ./data/ravdess/ravdess_test.csv
+test.type = csv
+test.absolute_path = False
+test.split_strategy = test
+target = emotion
+labels = ['angry', 'happy', 'neutral', 'sad']
+[FEATS]
+type = ['wavlm-large']
+no_reuse = False
+scale = standard
+[EXPL]
+sample_selection = all
+spotlight = True
+[MODEL]
+type = svm
\ No newline at end of file
diff --git a/tutorials/tut_stratify_emodb.ini b/tutorials/tut_stratify_emodb.ini
new file mode 100644
index 00000000..d7d5dea7
--- /dev/null
+++ b/tutorials/tut_stratify_emodb.ini
@@ -0,0 +1,29 @@
+[EXP]
+root = ./results/
+name = exp_emodb_wavlm_1
+runs = 1
+epochs = 1
+save = True
+[DATA]
+# the name of the database
+databases = ['emodb']
+# the location of the data
+emodb = ./data/emodb/emodb
+# set the split strategy to "balanced"
+emodb.split_strategy = balanced
+# set a percentage value for your test split
+emodb.test_size = 20
+# stratify variables with weights for importance
+balance = {'emotion':2, 'age':1, 'gender':1}
+# all stratification variables need to be categorical, 
+# so we need to state the number of bins for "age" 
+age_bins = 2
+# a value for how much importance to give for the ideal group sizes
+size_diff_weight = 1
+# the target value of the experiment
+target = emotion
+[FEATS]
+type = ['wavlm-large']
+scale = standard
+[MODEL]
+type = svm
\ No newline at end of file


Test patch:
diff --git a/nkululeko/test_pretrain.py b/nkululeko/test_pretrain.py
index 4854f7b7..6578eb50 100644
--- a/nkululeko/test_pretrain.py
+++ b/nkululeko/test_pretrain.py
@@ -1,24 +1,23 @@
 # test_pretrain.py
 import argparse
 import configparser
+import json
 import os.path
 
+import audeer
+import audiofile
+import audmetric
 import datasets
 import numpy as np
 import pandas as pd
 import torch
 import transformers
 
-import audeer
-import audiofile
-import audmetric
-
-from nkululeko.constants import VERSION
 import nkululeko.experiment as exp
-import nkululeko.models.finetune_model as fm
 import nkululeko.glob_conf as glob_conf
+import nkululeko.models.finetune_model as fm
+from nkululeko.constants import VERSION
 from nkululeko.utils.util import Util
-import json
 
 
 def doit(config_file):