But all those things are coding practices.
I can code a nn.Module called preprocessing and just set it as a layer which is straight-forward to remove (and define), or as a method within the model class. Even the forward pass of a “network” can be modularised or to be a messy script.
Anyway there is no correct answer here. I used to have many modules to do the preprocessing and so on and ended up being problematic when you try to make the code user-friendly.
Then it becomes something like hey, you need this model + this preprocessing funcs. And then,  one usually have a utils file, a custom package or similar that people who want to reuse your model will have to copy and understand anyway.
And it’s very often not very well written. You may find reading + cropping in the same function. Turns out you want to use another library to read.
I ended up writting wrappers around the modules which contains all this with pytorch functions. Do you want to reuse the model? Are you a basic user? Copy paste the wrapper and pass audio, video, images… and don’t struggle more.
Do you want just the “neural network”? Then just copy that.
Do you want the whole training pipeline with the dataloader and my setup? Then just clone the repo.
For example I recently found an interesting audiovisual transformer from facebook. The code is a mess. It works based on a library which creates an object which carries all the cfg. Really hard to understand how does it work unless you inspect the object in a debug session. Everything ofc fused with a distributed dataloader. You cannot just copy the model. To me it would have been much simpler if they just created and isolated model I can run with my inputs.