diff --git a/model_server/extensions/chaeo/roiset.py b/model_server/base/roiset.py
similarity index 94%
rename from model_server/extensions/chaeo/roiset.py
rename to model_server/base/roiset.py
index c478ff2598527216e85857b9f448eeea73372ae7..bf40227a0629c2d3747e1faf20360565fa664a63 100644
--- a/model_server/extensions/chaeo/roiset.py
+++ b/model_server/base/roiset.py
@@ -1,9 +1,11 @@
 from math import sqrt, floor
 from pathlib import Path
+from typing import List, Union
 from uuid import uuid4
 
 import numpy as np
 import pandas as pd
+from pydantic import BaseModel
 from scipy.stats import moment
 from skimage.filters import sobel
 
@@ -15,10 +17,51 @@ from model_server.base.accessors import GenericImageDataAccessor, InMemoryDataAc
 from model_server.base.models import InstanceSegmentationModel
 from model_server.base.process import pad, rescale, resample_to_8bit, make_rgb
 from model_server.extensions.chaeo.annotators import draw_box_on_patch, draw_contours_on_patch, draw_boxes_on_3d_image
-from model_server.extensions.chaeo.params import RoiFilter, RoiSetMetaParams, RoiSetExportParams
 from model_server.extensions.chaeo.accessors import write_patch_to_file, MonoPatchStack, Multichannel3dPatchStack
 from model_server.extensions.chaeo.process import mask_largest_object
 
+class PatchParams(BaseModel):
+    draw_bounding_box: bool = False
+    draw_contour: bool = False
+    draw_mask: bool = False
+    rescale_clip: float = 0.001
+    focus_metric: str = 'max_sobel'
+    rgb_overlay_channels: List[Union[int, None]] = [None, None, None]
+    rgb_overlay_weights: List[float] = [1.0, 1.0, 1.0]
+    pad_to: int = 256
+
+
+class AnnotatedZStackParams(BaseModel):
+    draw_label: bool = False
+
+
+class RoiFilterRange(BaseModel):
+    min: float
+    max: float
+
+
+class RoiFilter(BaseModel):
+    area: Union[RoiFilterRange, None] = None
+    solidity: Union[RoiFilterRange, None] = None
+
+
+class RoiSetMetaParams(BaseModel):
+    filters: Union[RoiFilter, None] = None
+    expand_box_by: List[int] = [128, 0]
+
+
+class RoiSetExportParams(BaseModel):
+    pixel_probabilities: bool = False
+    patches_3d: Union[PatchParams, None] = None
+    annotated_patches_2d: Union[PatchParams, None] = None
+    patches_2d: Union[PatchParams, None] = None
+    patch_masks: Union[PatchParams, None] = None
+    annotated_zstacks: Union[AnnotatedZStackParams, None] = None
+    object_classes: bool = False
+    dataframe: bool = False
+
+
+
 
 def _get_label_ids(acc_seg_mask: GenericImageDataAccessor) -> InMemoryDataAccessor:
     return InMemoryDataAccessor(label(acc_seg_mask.data[:, :, 0, 0]).astype('uint16'))
@@ -516,5 +559,3 @@ def project_stack_from_focal_points(
     )
 
 
-
-
diff --git a/model_server/extensions/chaeo/params.py b/model_server/extensions/chaeo/params.py
deleted file mode 100644
index 9e06b286498a21bf1115b15f26f99b10547d9808..0000000000000000000000000000000000000000
--- a/model_server/extensions/chaeo/params.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from typing import List, Union
-
-from pydantic import BaseModel
-
-
-class PatchParams(BaseModel):
-    draw_bounding_box: bool = False
-    draw_contour: bool = False
-    draw_mask: bool = False
-    rescale_clip: float = 0.001
-    focus_metric: str = 'max_sobel'
-    rgb_overlay_channels: List[Union[int, None]] = [None, None, None]
-    rgb_overlay_weights: List[float] = [1.0, 1.0, 1.0]
-    pad_to: int = 256
-
-
-class AnnotatedZStackParams(BaseModel):
-    draw_label: bool = False
-
-class RoiFilterRange(BaseModel):
-    min: float
-    max: float
-
-class RoiFilter(BaseModel):
-    area: Union[RoiFilterRange, None] = None
-    solidity: Union[RoiFilterRange, None] = None
-
-
-class RoiSetMetaParams(BaseModel):
-    filters: Union[RoiFilter, None] = None
-    expand_box_by: List[int] = [128, 0]
-
-
-class RoiSetExportParams(BaseModel):
-    pixel_probabilities: bool = False
-    patches_3d: Union[PatchParams, None] = None
-    annotated_patches_2d: Union[PatchParams, None] = None
-    patches_2d: Union[PatchParams, None] = None
-    patch_masks: Union[PatchParams, None] = None
-    annotated_zstacks: Union[AnnotatedZStackParams, None] = None
-    object_classes: bool = False
-    dataframe: bool = False
-
-
diff --git a/model_server/extensions/chaeo/products.py b/model_server/extensions/chaeo/products.py
deleted file mode 100644
index b28b04f643122b019e912540f228c8ed20be9eeb..0000000000000000000000000000000000000000
--- a/model_server/extensions/chaeo/products.py
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/model_server/extensions/chaeo/tests/test_roiset_workflow.py b/model_server/extensions/chaeo/tests/test_roiset_workflow.py
new file mode 100644
index 0000000000000000000000000000000000000000..369701def197a92e470e456142bcfade57ef2c9b
--- /dev/null
+++ b/model_server/extensions/chaeo/tests/test_roiset_workflow.py
@@ -0,0 +1,71 @@
+import unittest
+
+from model_server.base.models import DummyInstanceSegmentationModel
+from model_server.base.roiset import RoiSetMetaParams, RoiSetExportParams
+from model_server.conf.testing import output_path
+from model_server.extensions.chaeo.conf.testing import multichannel_zstack, pipeline_params
+from model_server.extensions.chaeo.workflows import infer_object_map_from_zstack
+
+from tests.test_roiset import BaseTestRoiSetMonoProducts
+
+class TestRoiSetWorkflow(BaseTestRoiSetMonoProducts, unittest.TestCase):
+
+    def test_object_map_workflow(self):
+        pp = pipeline_params
+        models = [
+            self.pxmodel,
+            DummyInstanceSegmentationModel(),
+        ]
+
+        models = {
+            'pixel_classifier': {
+                'model': self.pxmodel,
+                'params': {
+                    'px_class': 0,
+                    'px_prob_threshold': 0.6,
+                }
+            },
+            'object_classifier': {
+                'name': 'dummy',
+                'model': DummyInstanceSegmentationModel(),
+            }
+        }
+
+        roi_params = RoiSetMetaParams(**{
+            'mask_type': 'boxes',
+            'filters': {
+                'area': {'min': 1e3, 'max': 1e8}
+            },
+            'expand_box_by': [128, 2]
+        })
+
+        export_params = RoiSetExportParams(**{
+            'pixel_probabilities': True,
+            'patches_3d': {},
+            'annotated_patches_2d': {
+                'draw_bounding_box': True,
+                'rgb_overlay_channels': [3, None, None],
+                'rgb_overlay_weights': [0.2, 1.0, 1.0],
+                'pad_to': 512,
+            },
+            'patches_2d': {
+                'draw_bounding_box': False,
+                'draw_mask': False,
+            },
+            'patch_masks': {
+                'pad_to': 256,
+            },
+            'annotated_zstacks': {},
+            'object_classes': True,
+            'dataframe': True,
+        })
+
+        infer_object_map_from_zstack(
+            multichannel_zstack['path'],
+            output_path / 'roiset' / 'workflow',
+            models,
+            segmentation_channel=pp['segmentation_channel'],
+            patches_channel=pp['patches_channel'],
+            export_params=export_params,
+            roi_params=roi_params,
+        )
\ No newline at end of file
diff --git a/model_server/extensions/chaeo/workflows.py b/model_server/extensions/chaeo/workflows.py
index 08d06adf07dbba82c84286a32d7154d1c1070ea1..b837877979017384effdb840c1f171d7e6d1d808 100644
--- a/model_server/extensions/chaeo/workflows.py
+++ b/model_server/extensions/chaeo/workflows.py
@@ -8,9 +8,9 @@ from skimage.measure import label
 from skimage.morphology import dilation
 from sklearn.model_selection import train_test_split
 
-from extensions.chaeo.params import RoiSetExportParams, RoiSetMetaParams
+from base.roiset import RoiSetMetaParams, RoiSetExportParams
 from model_server.extensions.chaeo.process import mask_largest_object
-from model_server.extensions.chaeo.roiset import _get_label_ids, RoiSet
+from base.roiset import _get_label_ids, RoiSet
 
 from model_server.base.accessors import generate_file_accessor, InMemoryDataAccessor, write_accessor_data_to_file
 from model_server.base.models import Model, InstanceSegmentationModel, SemanticSegmentationModel
@@ -68,7 +68,8 @@ def infer_object_map_from_zstack(
         'output_path': output_folder_path,
     }
 
-# TODO: to app-specific ecotaxa module
+
+
 def transfer_ecotaxa_labels_to_patch_stacks(
     where_masks: str,
     where_patches: str,
@@ -172,6 +173,4 @@ def transfer_ecotaxa_labels_to_patch_stacks(
         for k in zstacks.keys():
             write_accessor_data_to_file(Path(where_output) / f'zstack_{k}.tif', InMemoryDataAccessor(zstacks[k]))
 
-        pd.DataFrame(stack_meta).to_csv(Path(where_output) / f'{dfk}_stack.csv', index=False)
-
-
+        pd.DataFrame(stack_meta).to_csv(Path(where_output) / f'{dfk}_stack.csv', index=False)
\ No newline at end of file
diff --git a/model_server/extensions/chaeo/tests/test_zstack.py b/tests/test_roiset.py
similarity index 81%
rename from model_server/extensions/chaeo/tests/test_zstack.py
rename to tests/test_roiset.py
index f3b82997482884e70e12bd341dbc732c5aa745dc..df77c111ec69b372d6778465cc119d385aae3316 100644
--- a/model_server/extensions/chaeo/tests/test_zstack.py
+++ b/tests/test_roiset.py
@@ -6,9 +6,8 @@ from pathlib import Path
 from model_server.conf.testing import output_path
 
 from model_server.extensions.chaeo.conf.testing import multichannel_zstack, pixel_classifier, pipeline_params
-from extensions.chaeo.params import RoiSetExportParams, RoiSetMetaParams
-from model_server.extensions.chaeo.workflows import infer_object_map_from_zstack
-from model_server.extensions.chaeo.roiset import _get_label_ids, RoiSet
+from model_server.base.roiset import RoiSetMetaParams
+from model_server.base.roiset import _get_label_ids, RoiSet
 from model_server.base.accessors import generate_file_accessor, InMemoryDataAccessor, write_accessor_data_to_file
 from model_server.extensions.ilastik.models import IlastikPixelClassifierModel
 from model_server.base.models import DummyInstanceSegmentationModel
@@ -131,7 +130,7 @@ class TestRoiSetMonoProducts(BaseTestRoiSetMonoProducts, unittest.TestCase):
         roiset = RoiSet(self.stack_ch_pa, id_map, params=RoiSetMetaParams(mask_type='boxes'))
         df = roiset.get_df()
 
-        from model_server.extensions.chaeo.roiset import project_stack_from_focal_points
+        from base.roiset import project_stack_from_focal_points
 
         img = project_stack_from_focal_points(
             df['centroid-0'].to_numpy(),
@@ -159,66 +158,6 @@ class TestRoiSetMonoProducts(BaseTestRoiSetMonoProducts, unittest.TestCase):
         self.assertTrue(all(roiset.get_df()['classify_by_dummy_class'].unique() == [1]))
         self.assertTrue(all(np.unique(roiset.object_class_maps['dummy_class'].data) == [0, 1]))
 
-    def test_object_map_workflow(self):
-        pp = pipeline_params
-        models = [
-            self.pxmodel,
-            DummyInstanceSegmentationModel(),
-        ]
-
-        models = {
-            'pixel_classifier': {
-                'model': self.pxmodel,
-                'params': {
-                    'px_class': 0,
-                    'px_prob_threshold': 0.6,
-                }
-            },
-            'object_classifier': {
-                'name': 'dummy',
-                'model': DummyInstanceSegmentationModel(),
-            }
-        }
-
-        roi_params = RoiSetMetaParams(**{
-            'mask_type': 'boxes',
-            'filters': {
-                'area': {'min': 1e3, 'max': 1e8}
-            },
-            'expand_box_by': [128, 2]
-        })
-
-        export_params = RoiSetExportParams(**{
-            'pixel_probabilities': True,
-            'patches_3d': {},
-            'annotated_patches_2d': {
-                'draw_bounding_box': True,
-                'rgb_overlay_channels': [3, None, None],
-                'rgb_overlay_weights': [0.2, 1.0, 1.0],
-                'pad_to': 512,
-            },
-            'patches_2d': {
-                'draw_bounding_box': False,
-                'draw_mask': False,
-            },
-            'patch_masks': {
-                'pad_to': 256,
-            },
-            'annotated_zstacks': {},
-            'object_classes': True,
-            'dataframe': True,
-        })
-
-        infer_object_map_from_zstack(
-            multichannel_zstack['path'],
-            output_path / 'roiset' / 'workflow',
-            models,
-            segmentation_channel=pp['segmentation_channel'],
-            patches_channel=pp['patches_channel'],
-            export_params=export_params,
-            roi_params=roi_params,
-        )
-
 
 class TestRoiSetMultichannelProducts(BaseTestRoiSetMonoProducts, unittest.TestCase):