Arpan Tripathi commited on
Commit
f0c392f
ยท
unverified ยท
1 Parent(s): 20296e4

replace base miner worker with docker-compose setup (#149)

Browse files
.gitignore CHANGED
@@ -11,7 +11,6 @@ note.txt
11
  *.mp3
12
  *.js
13
  *.xml
14
- *.txt
15
  .DS_Store
16
  *.json
17
  .env
 
11
  *.mp3
12
  *.js
13
  *.xml
 
14
  .DS_Store
15
  *.json
16
  .env
docs/miner_setup.md CHANGED
@@ -1,6 +1,12 @@
1
  # Running Miner
2
 
3
- A high-performance decentralized video processing miner that leverages **Video2X** for AI-powered video upscaling. This guide provides detailed instructions to set up, configure, and run the miner effectively
 
 
 
 
 
 
4
 
5
  ---
6
 
@@ -10,264 +16,250 @@ To achieve optimal results, we recommend the following setup:
10
 
11
  - **Operating System**: Ubuntu 24.04 LTS or higher
12
  [Learn more about Ubuntu 24.04 LTS](https://ubuntu.com/blog/tag/ubuntu-24-04-lts)
13
- - **GPU**: NVIDIA RTX 4090 or higher
14
- (Required for efficient video upscaling using Video2X)
 
 
 
15
 
16
  ---
17
 
18
- ## Install PM2 (Process Manager)
19
 
20
- **PM2** is used to manage and monitor the miner process. If you havenโ€™t installed PM2 yet, follow these steps:
21
 
22
- 1. Install `npm` and PM2:
23
- ```bash
24
- sudo apt update
25
- sudo apt install npm -y
26
- sudo npm install pm2 -g
27
- pm2 update
28
- ```
29
 
30
- 2. For more details, refer to the [PM2 Documentation](https://pm2.io/docs/runtime/guide/installation/).
31
 
32
- ---
33
 
34
- ## Install Redis
 
 
35
 
36
- 1. Install 'redis'
37
- ```bash
38
- sudo apt update
39
- sudo apt install redis-server
40
- sudo systemctl start redis
41
- sudo systemctl enable redis-server
42
- sudo systemctl status redis
43
- ```
44
 
45
- ## Install Project Dependencies
46
 
47
- ### Prerequisites
48
 
49
- - **Python**: Version 3.10 or higher
50
- - **pip**: Python package manager
51
- - **virtualenv** (optional): For dependency isolation
 
52
 
53
  ---
54
 
 
 
55
  ### 1. Clone the Repository
56
 
57
- Clone the project repository to your local machine:
58
  ```bash
59
  git clone https://github.com/vidaio-subnet/vidaio-subnet.git
60
  cd vidaio-subnet
61
  ```
62
 
63
- ---
64
-
65
- ### 2. Set Up a Virtual Environment (Recommended)
66
 
67
- Create and activate a virtual environment to isolate project dependencies:
68
  ```bash
69
  python3 -m venv venv
70
- source venv/bin/activate
71
  ```
72
 
73
- ---
74
-
75
- ### 3. Install the Package and Dependencies
76
 
77
- Install the project and its dependencies using `pip`:
78
  ```bash
79
  pip install -e .
80
  ```
81
 
82
  ---
83
 
84
- ### 4. Configure Environment Variables
85
-
86
- To configure environment variables, follow these steps:
87
 
88
- 1. Create a `.env` file in the project root directory by referencing the provided `.env.template` file:
89
- ```bash
90
- cp .env.template .env
91
- ```
92
 
93
- 2. Set up a bucket in cloud storage. The base miner code utilizes MinIO to connect with cloud storage services, so you'll need to prepare your bucket using a platform that supports MinIO integration, such as Backblaze. Alternatively, you can modify the code to suit your specific requirements.
94
- 3. Add the required variables to the `.env` file. For example:
95
- ```env
96
- BUCKET_NAME="S3 buckent name"
97
- BUCKET_COMPATIBLE_ENDPOINT="S3 bucket endpoint"
98
- BUTKET_COMPATIBLE_ACCESS_KEY="S3 bucket personal access key"
99
- BUCKET_COMPATIBLE_SECRET_KEY="S3 bucket personal secret key"
100
- PEXELS_API_KEY="Your Pexels account api key"
101
- WANDB_API_KEY="Your WANDB account api key"
102
- ```
103
 
104
- 4. Ensure that the bucket is configured with the appropriate permissions to allow file uploads and enable public access for downloads via presigned URLs.
105
-
106
- 5. Once the `.env` file is properly configured, the application will use the specified credentials for S3 bucket and Pexels.
107
-
108
-
109
- ---
110
 
111
- ## Install Video2X
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- The miner requires **Video2X** for AI-powered video upscaling. Follow the steps below to install and configure Video2X.
114
 
115
- ---
116
 
117
- ### Step 1: Install FFMPEG
118
-
119
- FFMPEG is required for processing video files. Install it using the following commands:
120
  ```bash
121
- sudo apt update
122
- sudo apt install ffmpeg -y
123
  ```
124
 
125
- For more details, refer to the [FFMPEG Documentation](https://www.ffmpeg.org/download.html#build-linux).
126
-
127
  ---
128
 
129
- ### Step 2: Install CUDA and NVCC
130
 
131
- Ensure your CUDA drivers and NVCC (NVIDIA Compiler) are properly installed and configured to support GPU acceleration.
132
 
133
- 1. Verify your CUDA installation:
134
- ```bash
135
- nvcc --version
136
- ```
137
 
138
- 2. Ensure you CUDA driver is installed correctly
139
- <!-- Install or update the CUDA drivers if they are not already installed:
140
- ```bash
141
- sudo apt update
142
- sudo apt install nvidia-cuda-toolkit -y
143
- ``` -->
144
 
145
- For more information, refer to the [CUDA Toolkit Installation Guide](https://developer.nvidia.com/cuda-toolkit).
146
 
147
- ---
 
 
148
 
149
- ### Step 3: Install Video2X
150
 
151
- ---
 
 
152
 
153
- ### Option 1: Install Video2X by downloading Debian Package:
154
 
 
155
 
156
- 1. **Download the Video2X `.deb` package**:
157
- ```bash
158
- wget -P services/upscaling/models https://github.com/k4yt3x/video2x/releases/download/6.3.1/video2x-linux-ubuntu2404-amd64.deb
159
- ```
160
 
161
- 2. **Install the package using `dpkg`**:
162
- ```bash
163
- sudo dpkg -i services/upscaling/models/video2x-linux-ubuntu2404-amd64.deb
164
- ```
165
 
166
- 3. **Resolve dependencies** (if any):
167
- ```bash
168
- sudo apt-get install -f
169
- ```
170
 
171
- ### Option 2: Install Video2X by Building Debian Package:
172
 
173
- 1. Install Cargo
174
- Cargo is required to build the Video2X package:
175
  ```bash
176
- sudo apt-get update
177
- sudo apt-get install cargo -y
178
- cargo install just --version=1.39.0
179
  ```
180
 
181
- 2. Clone the Video2X Repository
182
- you can clone this repository within the current vidaio-subnet package
183
- ```bash
184
- git clone --recurse-submodules https://github.com/vidAio-subnet/video2x
185
- cd video2x
186
- ```
187
 
188
- 3. Build the Video2X Project
189
- Before building, ensure `~/.cargo/bin` is included in your `PATH` environment variable:
190
- ```bash
191
- export PATH="$HOME/.cargo/bin:$PATH"
192
- ```
193
 
194
- Run the following command to build the package:
195
  ```bash
196
- just ubuntu2404
197
  ```
198
 
199
- Once the build is complete, the `.deb` package will be located in the current directory.
 
 
200
 
201
- 4. Install the Built Package
202
- Install the `.deb` package using:
203
  ```bash
204
- sudo dpkg -i video2x-linux-ubuntu-amd64.deb
205
  ```
206
 
207
  ---
208
- For additional details, refer to the [Video2X Documentation](https://docs.video2x.org/building/linux.html).
209
 
210
- ## Running the Video Upscaling Endpoint
211
 
212
- You can run the video upscaling endpoint using **PM2** to manage the process:
213
 
214
  ```bash
215
- pm2 start "python services/upscaling/server.py" --name video-upscaler
 
 
216
  ```
217
 
218
- ### Notes:
219
- - The `video-upscaler` process will handle video upscaling requests.
220
- - Use the following PM2 commands to manage the process:
221
- - **View Logs**: `pm2 logs video-upscaler`
222
- - **Restart**: `pm2 restart video-upscaler`
223
- - **Stop**: `pm2 stop video-upscaler`
224
 
225
- ---
226
-
227
- ## Running the Video Compression Endpoint
 
228
 
229
- You can also run the video Compression endpoint using **PM2** to manage the process:
230
 
231
  ```bash
232
- pm2 start "python services/compress/server.py" --name video-compressor
 
 
233
  ```
234
 
 
 
235
  ---
236
 
237
- ## Running the file deletion process
238
 
239
- You can run the file deletion process using **PM2** to manage the process:
240
 
241
  ```bash
242
- pm2 start "python services/miner_utilities/file_deletion_server.py" --name video-deleter
 
 
 
 
 
 
243
  ```
244
 
245
- ## Running the Miner with PM2
246
 
247
- To run the miner, use the following command:
248
 
249
  ```bash
250
  pm2 start "python3 neurons/miner.py --wallet.name [Your_Wallet_Name] --wallet.hotkey [Your_Hotkey_Name] --subtensor.network finney --netuid 85 --axon.port [port] --logging.debug" --name video-miner
251
  ```
252
 
253
- ### Parameters:
254
- - **`--wallet.name`**: Replace `[Your_Wallet_Name]` with your wallet name.
255
- - **`--wallet.hotkey`**: Replace `[Your_Hotkey_Name]` with your hotkey name.
256
- - **`--subtensor.network`**: Specify the target network (e.g., `finney`).
257
- - **`--netuid`**: Specify the network UID (e.g., `292`).
258
- - **`--axon.port`**: Replace `[port]` with the desired port number.
259
- - **`--logging.debug`**: Enables debug-level logging for detailed output.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
- ### Managing the Miner Process:
262
- - **Start the Miner**: The above command will start the miner as a PM2 process named `video-miner`.
263
- - **View Logs**: Use `pm2 logs video-miner` to monitor miner logs in real time.
264
- - **Restart the Miner**: Use `pm2 restart video-miner` to restart the process.
265
- - **Stop the Miner**: Use `pm2 stop video-miner` to stop the process.
266
 
267
  ---
268
 
269
  ## Additional Notes
270
 
271
- - Ensure all dependencies are installed and configured correctly before running the miner.
272
- - Use a high-performance GPU and sufficient system resources for optimal performance.
273
- - For troubleshooting and debugging, refer to the logs available in PM2.
 
 
 
 
1
  # Running Miner
2
 
3
+ A high-performance decentralized video processing miner that forwards validator requests to containerized video services. The current miner framework supports:
4
+
5
+ - **Video2X upscaling** through the `upscaling-video2x` Docker Compose profile.
6
+ - **FFmpeg upscaling** through the `upscaling-ffmpeg` Docker Compose profile.
7
+ - **FFmpeg compression** through the `compression` Docker Compose service.
8
+
9
+ `neurons/miner.py` no longer needs the old PM2-managed `services/upscaling/server.py` or `services/compress/server.py` workers. It forwards directly to the service container ports.
10
 
11
  ---
12
 
 
16
 
17
  - **Operating System**: Ubuntu 24.04 LTS or higher
18
  [Learn more about Ubuntu 24.04 LTS](https://ubuntu.com/blog/tag/ubuntu-24-04-lts)
19
+ - **GPU**: NVIDIA RTX 4090 or higher
20
+ - **VRAM**: At least 16 GB per GPU
21
+ - **Docker** and **Docker Compose**
22
+ - **NVIDIA drivers** and `nvidia-container-toolkit`
23
+ - **Python**: Version 3.10 or higher for running the miner process
24
 
25
  ---
26
 
27
+ ## Install System Dependencies
28
 
29
+ Install common host dependencies:
30
 
31
+ ```bash
32
+ sudo apt update
33
+ sudo apt install -y git curl npm python3 python3-venv python3-pip
34
+ ```
 
 
 
35
 
36
+ Install Docker and Docker Compose using the official Docker instructions for your host, then install NVIDIA Container Toolkit so Compose services can access the GPU.
37
 
38
+ Verify Docker can see the GPU:
39
 
40
+ ```bash
41
+ docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
42
+ ```
43
 
44
+ ---
 
 
 
 
 
 
 
45
 
46
+ ## Install PM2
47
 
48
+ PM2 is optional for the processing containers, but useful for managing the miner process itself.
49
 
50
+ ```bash
51
+ sudo npm install pm2 -g
52
+ pm2 update
53
+ ```
54
 
55
  ---
56
 
57
+ ## Install Project Dependencies
58
+
59
  ### 1. Clone the Repository
60
 
 
61
  ```bash
62
  git clone https://github.com/vidaio-subnet/vidaio-subnet.git
63
  cd vidaio-subnet
64
  ```
65
 
66
+ ### 2. Set Up a Virtual Environment
 
 
67
 
 
68
  ```bash
69
  python3 -m venv venv
70
+ source venv/bin/activate
71
  ```
72
 
73
+ ### 3. Install the Package
 
 
74
 
 
75
  ```bash
76
  pip install -e .
77
  ```
78
 
79
  ---
80
 
81
+ ## Configure Storage
 
 
82
 
83
+ The miner process downloads validator payload URLs, places local files in a shared work directory, forwards local paths to the processing containers, uploads processed outputs to S3-compatible storage, and returns presigned URLs. The processing containers run without egress.
 
 
 
84
 
85
+ Create a miner service environment file:
 
 
 
 
 
 
 
 
 
86
 
87
+ ```bash
88
+ cp miner/.env.template miner/.env
89
+ ```
 
 
 
90
 
91
+ Edit `miner/.env` with your storage credentials:
92
+
93
+ ```env
94
+ COMPOSE_PROJECT_NAME=miner
95
+ MINER_SHARED_DIR=/tmp/vidaio-miner-video-tmp
96
+ MINER_STORAGE_PROVIDER=backblaze
97
+ MINER_STORAGE_S3_ACCESS_KEY_ID=your-access-key
98
+ MINER_STORAGE_S3_SECRET_ACCESS_KEY=your-secret-key
99
+ MINER_STORAGE_S3_REGION=us-east-005
100
+ MINER_STORAGE_S3_BUCKET_NAME=your-bucket-name
101
+ MINER_STORAGE_S3_ENDPOINT_URL=https://s3.us-east-005.backblazeb2.com
102
+ ```
103
 
104
+ For AWS S3, use the same variable names and leave `MINER_STORAGE_S3_ENDPOINT_URL` blank.
105
 
106
+ Create the shared work directory before starting the miner and containers:
107
 
 
 
 
108
  ```bash
109
+ mkdir -p /tmp/vidaio-miner-video-tmp
110
+ chmod 777 /tmp/vidaio-miner-video-tmp
111
  ```
112
 
 
 
113
  ---
114
 
115
+ ## Start Processing Services
116
 
117
+ Run Docker Compose commands from the `miner` directory:
118
 
119
+ ```bash
120
+ cd miner
121
+ ```
 
122
 
123
+ ### Option A: Video2X Upscaling Miner
 
 
 
 
 
124
 
125
+ Use this profile if you want Video2X-based upscaling. Complete the host/container setup first:
126
 
127
+ ```bash
128
+ cat upscaling/SETUP.md
129
+ ```
130
 
131
+ After running all the commands listed above, start the Video2X upscaling service:
132
 
133
+ ```bash
134
+ docker compose --profile upscaling-video2x up -d upscaling-video2x
135
+ ```
136
 
137
+ The service is exposed on `http://localhost:8003`.
138
 
139
+ ### Option B: FFmpeg Upscaling Miner
140
 
141
+ Use this profile if you want FFmpeg-based upscaling instead of Video2X:
 
 
 
142
 
143
+ ```bash
144
+ docker compose --profile upscaling-ffmpeg up -d upscaling-ffmpeg
145
+ ```
 
146
 
147
+ The FFmpeg upscaling service is exposed on `http://localhost:8005`.
 
 
 
148
 
149
+ Before starting `neurons/miner.py`, point the miner at that port:
150
 
 
 
151
  ```bash
152
+ export MINER_UPSCALING_SERVICE_URL=http://localhost:8005
 
 
153
  ```
154
 
155
+ ### Option C: Compression Miner
 
 
 
 
 
156
 
157
+ Start the compression service:
 
 
 
 
158
 
 
159
  ```bash
160
+ docker compose up -d compression
161
  ```
162
 
163
+ The compression service is exposed on `http://localhost:8004`.
164
+
165
+ Return to the repository root before launching the miner:
166
 
 
 
167
  ```bash
168
+ cd ..
169
  ```
170
 
171
  ---
 
172
 
173
+ ## Miner Forwarding Configuration
174
 
175
+ `neurons/miner.py` forwards requests to these service URLs:
176
 
177
  ```bash
178
+ export MINER_UPSCALING_SERVICE_URL=http://localhost:8003
179
+ export MINER_COMPRESSION_SERVICE_URL=http://localhost:8004
180
+ export MINER_SHARED_VOLUME_PATH=/tmp/vidaio-miner-video-tmp
181
  ```
182
 
183
+ Defaults:
 
 
 
 
 
184
 
185
+ - `MINER_UPSCALING_SERVICE_URL` defaults to `http://localhost:8003` for Video2X upscaling.
186
+ - `MINER_COMPRESSION_SERVICE_URL` defaults to `http://localhost:8004`.
187
+ - `MINER_SHARED_VOLUME_PATH` defaults to `MINER_SHARED_DIR`, then `/tmp/vidaio-miner-video-tmp`.
188
+ - For FFmpeg upscaling, override `MINER_UPSCALING_SERVICE_URL` to `http://localhost:8005`.
189
 
190
+ Useful service checks:
191
 
192
  ```bash
193
+ curl -sf http://localhost:8003/health
194
+ curl -sf http://localhost:8004/health
195
+ curl -sf http://localhost:8005/health
196
  ```
197
 
198
+ Only the services you started need to pass health checks.
199
+
200
  ---
201
 
202
+ ## Run the Miner
203
 
204
+ Start the miner from the repository root:
205
 
206
  ```bash
207
+ python3 neurons/miner.py \
208
+ --wallet.name [Your_Wallet_Name] \
209
+ --wallet.hotkey [Your_Hotkey_Name] \
210
+ --subtensor.network finney \
211
+ --netuid 85 \
212
+ --axon.port [port] \
213
+ --logging.debug
214
  ```
215
 
216
+ ### Run the Miner with PM2
217
 
218
+ Export any `MINER_*_SERVICE_URL` overrides in the same shell before starting PM2. If you change those variables later, restart with `--update-env`.
219
 
220
  ```bash
221
  pm2 start "python3 neurons/miner.py --wallet.name [Your_Wallet_Name] --wallet.hotkey [Your_Hotkey_Name] --subtensor.network finney --netuid 85 --axon.port [port] --logging.debug" --name video-miner
222
  ```
223
 
224
+ ### Parameters
225
+
226
+ - `--wallet.name`: Replace `[Your_Wallet_Name]` with your wallet name.
227
+ - `--wallet.hotkey`: Replace `[Your_Hotkey_Name]` with your hotkey name.
228
+ - `--subtensor.network`: Specify the target network, usually `finney`.
229
+ - `--netuid`: Specify the network UID, currently `85`.
230
+ - `--axon.port`: Replace `[port]` with the desired public axon port.
231
+ - `--logging.debug`: Enables debug-level logging.
232
+
233
+ ---
234
+
235
+ ## Managing Services
236
+
237
+ Processing containers:
238
+
239
+ ```bash
240
+ cd miner
241
+ docker compose ps
242
+ docker compose logs -f upscaling-video2x
243
+ docker compose logs -f upscaling-ffmpeg
244
+ docker compose logs -f compression
245
+ docker compose down
246
+ ```
247
+
248
+ Miner process:
249
 
250
+ ```bash
251
+ pm2 logs video-miner
252
+ pm2 restart video-miner --update-env
253
+ pm2 stop video-miner
254
+ ```
255
 
256
  ---
257
 
258
  ## Additional Notes
259
 
260
+ - Run either `upscaling-video2x` or `upscaling-ffmpeg` for an upscaling miner. They expose different host ports but both implement `/upscale`.
261
+ - Run `compression` for a compression miner. It implements `/compress`.
262
+ - Processing containers publish localhost-only service ports and have `DISABLE_REMOTE_IO=true`; they only process local files from the shared work directory by default.
263
+ - The old PM2 service scripts for upscaling and compression are redundant with this container framework.
264
+ - For Video2X-specific preparation, use `miner/upscaling/SETUP.md`.
265
+ - For FFmpeg upscaling build/runtime notes, use `miner/upscaling/ffmpeg/SETUP.md`.
miner/.env.template ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ COMPOSE_PROJECT_NAME=miner
2
+ MINER_SHARED_DIR=/tmp/vidaio-miner-video-tmp
3
+
4
+ # S3-compatible storage settings
5
+ MINER_STORAGE_PROVIDER=backblaze
6
+ MINER_STORAGE_S3_ACCESS_KEY_ID=your-access-key
7
+ MINER_STORAGE_S3_SECRET_ACCESS_KEY=your-secret-key
8
+ MINER_STORAGE_S3_REGION=us-east-005
9
+ MINER_STORAGE_S3_BUCKET_NAME=your-bucket-name
10
+ MINER_STORAGE_S3_ENDPOINT_URL=https://s3.us-east-005.backblazeb2.com
11
+
12
+ # For AWS S3, keep the same variable names and leave the endpoint blank.
13
+ # MINER_STORAGE_S3_REGION=us-west-1
14
+ # MINER_STORAGE_S3_BUCKET_NAME=vidaiosubnet
15
+ # MINER_STORAGE_S3_ENDPOINT_URL=
miner/README.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Miner Quickstart
2
+
3
+ This project includes three miner services:
4
+ - `upscaling-video2x`: Upscaling service that wraps Video2X.
5
+ - `upscaling-ffmpeg`: Upscaling service using FFmpeg directly.
6
+ - `compression`: Compression microservice.
7
+
8
+ ## Prerequisites
9
+ - Run `sudo apt update`
10
+ - Docker + Docker Compose
11
+ - NVIDIA drivers and `nvidia-container-toolkit`
12
+ - Storage environment variables (S3-compatible) in a new `miner/.env` file derived from `miner/.env.template`
13
+ - A shared local work directory. The default is `/tmp/vidaio-miner-video-tmp` via `MINER_SHARED_DIR`.
14
+
15
+ The service APIs are published on localhost-only Docker ports. `neurons/miner.py` downloads validator payloads, places local files in `MINER_SHARED_DIR`, forwards local paths to the containers, uploads processed outputs, and returns presigned URLs. Remote URL input is disabled inside the service containers by default.
16
+
17
+ ```bash
18
+ mkdir -p /tmp/vidaio-miner-video-tmp
19
+ chmod 777 /tmp/vidaio-miner-video-tmp
20
+ ```
21
+
22
+ ## Video2X Upscaling Miner
23
+ 1. Complete host/container preparation in `miner/upscaling/SETUP.md`.
24
+ 2. Start service:
25
+ ```bash
26
+ docker compose --profile upscaling-video2x up -d upscaling-video2x
27
+ ```
28
+ 3. Point the miner at the Video2X profile port:
29
+ ```bash
30
+ export MINER_UPSCALING_SERVICE_URL=http://localhost:8003
31
+ ```
32
+
33
+ ## FFmpeg Upscaling Miner
34
+ 1. (Optional) Review build/runtime notes in `miner/upscaling/ffmpeg/SETUP.md`.
35
+ 2. Start service:
36
+ ```bash
37
+ docker compose --profile upscaling-ffmpeg up -d upscaling-ffmpeg
38
+ ```
39
+ 3. Point the miner at the FFmpeg profile port:
40
+ ```bash
41
+ export MINER_UPSCALING_SERVICE_URL=http://localhost:8005
42
+ ```
43
+
44
+ ## Compression Miner
45
+ ```bash
46
+ docker compose up -d compression
47
+ ```
48
+
49
+ ## Miner Forwarding
50
+ `neurons/miner.py` forwards directly to the container services:
51
+ - `MINER_UPSCALING_SERVICE_URL` defaults to `http://localhost:8003`.
52
+ - `MINER_COMPRESSION_SERVICE_URL` defaults to `http://localhost:8004`.
53
+ - `MINER_SHARED_VOLUME_PATH` defaults to `MINER_SHARED_DIR`, then `/tmp/vidaio-miner-video-tmp`.
miner/compression/Dockerfile ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM nvidia/cuda:13.0.0-devel-ubuntu22.04
2
+
3
+ # Build dependencies for ffmpeg + NVENC/NVDEC + codec libraries
4
+ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
5
+ build-essential ninja-build nasm perl g++ cmake wget unzip \
6
+ nvidia-cuda-dev nvidia-cuda-toolkit pkg-config \
7
+ libx264-dev libx265-dev libvpx-dev \
8
+ git python3-pip python3-venv curl \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Install meson (needed for dav1d build)
12
+ RUN pip3 install --no-cache-dir meson
13
+
14
+ # nv-codec-headers (NVENC/NVDEC API headers)
15
+ RUN wget https://github.com/FFmpeg/nv-codec-headers/archive/876af32a202d0de83bd1d36fe74ee0f7fcf86b0d.zip \
16
+ && unzip 876af32a202d0de83bd1d36fe74ee0f7fcf86b0d.zip \
17
+ && cd nv-codec-headers-876af32a202d0de83bd1d36fe74ee0f7fcf86b0d \
18
+ && make && make install && cd .. \
19
+ && rm -rf nv-codec-headers-* 876af32a202d0de83bd1d36fe74ee0f7fcf86b0d.zip
20
+
21
+ # dav1d (AV1 decoder)
22
+ RUN git clone --depth 1 --branch 1.4.2 https://code.videolan.org/videolan/dav1d.git \
23
+ && cd dav1d && mkdir build && cd build \
24
+ && meson setup .. --default-library=static --buildtype release \
25
+ && ninja && ninja install && ldconfig \
26
+ && cd ../.. && rm -rf dav1d
27
+
28
+ # SVT-AV1 (AV1 encoder for libsvtav1)
29
+ RUN git clone --depth 1 --branch v4.1.0 https://gitlab.com/AOMediaCodec/SVT-AV1.git \
30
+ && cd SVT-AV1 && mkdir build && cd build \
31
+ && cmake .. -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \
32
+ && make -j$(nproc) && make install && ldconfig \
33
+ && cd ../.. && rm -rf SVT-AV1
34
+
35
+ # Build ffmpeg (pinned commit 33b215d) with CUDA + x264 + x265 + dav1d + svtav1
36
+ RUN export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:/usr/local/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH" \
37
+ && git init ffmpeg && cd ffmpeg \
38
+ && git remote add origin https://github.com/FFmpeg/FFmpeg.git \
39
+ && git fetch --depth 1 origin 33b215d1554a14e87416a24f8e6034312e629af7 \
40
+ && git checkout FETCH_HEAD \
41
+ && ./configure \
42
+ --enable-gpl \
43
+ --enable-libx264 \
44
+ --enable-libx265 \
45
+ --enable-libvpx \
46
+ --enable-libdav1d \
47
+ --enable-libsvtav1 \
48
+ --enable-nonfree \
49
+ --enable-cuda-nvcc \
50
+ --enable-nvenc \
51
+ --enable-nvdec \
52
+ --extra-cflags="-I/usr/local/cuda/include" \
53
+ --extra-ldflags="-L/usr/local/cuda/lib64" \
54
+ --disable-static \
55
+ --enable-shared \
56
+ --nvccflags="-gencode arch=compute_75,code=sm_75 -gencode arch=compute_75,code=compute_75 -O2" \
57
+ && make -j$(nproc) && make install && ldconfig \
58
+ && cd .. && rm -rf ffmpeg
59
+
60
+ WORKDIR /app
61
+
62
+ COPY requirements.txt .
63
+ RUN pip install --no-cache-dir -r requirements.txt
64
+
65
+ COPY app.py .
66
+
67
+ EXPOSE 8004
68
+ CMD ["python3", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8004"]
miner/compression/app.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Compression microservice โ€” wraps the ffmpeg binary installed in this container.
3
+
4
+ Accepts a video path (on shared volume) OR a URL, codec, and quality settings,
5
+ runs GPU-accelerated ffmpeg compression, returns the output path or S3 URL.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import logging
12
+ import os
13
+ import uuid
14
+ from typing import Optional
15
+
16
+ import boto3
17
+ import httpx
18
+ from botocore.config import Config
19
+ from fastapi import FastAPI, HTTPException
20
+ from pydantic import BaseModel, Field
21
+
22
+ logging.basicConfig(
23
+ level=logging.INFO,
24
+ format="%(asctime)s | %(levelname)-7s | %(message)s",
25
+ datefmt="%Y-%m-%d %H:%M:%S",
26
+ )
27
+ log = logging.getLogger("compression")
28
+
29
+ app = FastAPI(title="Video Compression Service")
30
+
31
+ SHARED_VOLUME_PATH = os.getenv("SHARED_VOLUME_PATH", "/tmp/organic-proxy")
32
+ MAX_CONCURRENT = int(os.getenv("MAX_CONCURRENT_COMPRESSION", "2"))
33
+ DISABLE_REMOTE_IO = os.getenv("DISABLE_REMOTE_IO", "false").lower() in ("1", "true", "yes")
34
+
35
+ # Storage provider label only. Uploads use one S3-compatible code path.
36
+ STORAGE_PROVIDER = os.getenv("MINER_STORAGE_PROVIDER", "s3").lower()
37
+ S3_REGION = os.getenv("MINER_STORAGE_S3_REGION", "us-east-1").strip() or "us-east-1"
38
+ S3_BUCKET = os.getenv("MINER_STORAGE_S3_BUCKET_NAME", "").strip()
39
+ S3_ACCESS_KEY_ID = os.getenv("MINER_STORAGE_S3_ACCESS_KEY_ID", "").strip()
40
+ S3_SECRET_ACCESS_KEY = os.getenv("MINER_STORAGE_S3_SECRET_ACCESS_KEY", "").strip()
41
+ S3_ENDPOINT_URL = (
42
+ os.getenv("MINER_STORAGE_S3_ENDPOINT_URL")
43
+ or os.getenv("MINER_STORAGE_S3_ENDPOINT")
44
+ or ""
45
+ ).strip()
46
+ S3_PRESIGNED_EXPIRY = int(
47
+ os.getenv("MINER_STORAGE_S3_PRESIGNED_EXPIRY")
48
+ or os.getenv("S3_PRESIGNED_EXPIRY")
49
+ or "3600"
50
+ )
51
+
52
+ _semaphore = asyncio.Semaphore(MAX_CONCURRENT)
53
+ _queue_size = 0
54
+ _active_count = 0
55
+ _lock = asyncio.Lock()
56
+
57
+ # Codec name โ†’ nvenc/software encoder mapping
58
+ CODEC_MAP = {
59
+ "AV1": "av1_nvenc",
60
+ "H264": "h264_nvenc",
61
+ "H.264": "h264_nvenc",
62
+ "HEVC": "hevc_nvenc",
63
+ "H265": "hevc_nvenc",
64
+ "H.265": "hevc_nvenc",
65
+ "VP9": "libvpx-vp9",
66
+ }
67
+
68
+
69
+ def _is_url(path: str) -> bool:
70
+ return path.startswith("http://") or path.startswith("https://")
71
+
72
+
73
+ def _get_s3_client():
74
+ client_kwargs = {
75
+ "region_name": S3_REGION,
76
+ "aws_access_key_id": S3_ACCESS_KEY_ID or None,
77
+ "aws_secret_access_key": S3_SECRET_ACCESS_KEY or None,
78
+ "config": Config(signature_version="s3v4"),
79
+ }
80
+ if S3_ENDPOINT_URL:
81
+ client_kwargs["endpoint_url"] = S3_ENDPOINT_URL
82
+ return boto3.client("s3", **client_kwargs)
83
+
84
+
85
+ def _storage_config_status() -> dict[str, object]:
86
+ return {
87
+ "provider": STORAGE_PROVIDER,
88
+ "region": S3_REGION,
89
+ "bucket_configured": bool(S3_BUCKET),
90
+ "access_key_configured": bool(S3_ACCESS_KEY_ID),
91
+ "secret_key_configured": bool(S3_SECRET_ACCESS_KEY),
92
+ "endpoint_configured": bool(S3_ENDPOINT_URL),
93
+ }
94
+
95
+
96
+ def _validate_s3_config():
97
+ missing = []
98
+ if not S3_BUCKET:
99
+ missing.append("MINER_STORAGE_S3_BUCKET_NAME")
100
+ if not S3_ACCESS_KEY_ID:
101
+ missing.append("MINER_STORAGE_S3_ACCESS_KEY_ID")
102
+ if not S3_SECRET_ACCESS_KEY:
103
+ missing.append("MINER_STORAGE_S3_SECRET_ACCESS_KEY")
104
+
105
+ if missing:
106
+ raise RuntimeError(f"Missing storage configuration: {', '.join(missing)}")
107
+
108
+
109
+ async def _download_url(url: str, dest: str):
110
+ log.info(f"Downloading {url[:80]}... โ†’ {dest}")
111
+ async with httpx.AsyncClient(timeout=600.0, follow_redirects=True) as client:
112
+ async with client.stream("GET", url) as resp:
113
+ resp.raise_for_status()
114
+ with open(dest, "wb") as f:
115
+ async for chunk in resp.aiter_bytes(chunk_size=8192):
116
+ f.write(chunk)
117
+ log.info(f"Downloaded {os.path.getsize(dest) / (1024*1024):.1f} MB โ†’ {dest}")
118
+
119
+
120
+ def _upload_to_s3(local_path: str, key: str) -> str:
121
+ _validate_s3_config()
122
+ client = _get_s3_client()
123
+ client.upload_file(local_path, S3_BUCKET, key)
124
+ url = client.generate_presigned_url(
125
+ "get_object",
126
+ Params={"Bucket": S3_BUCKET, "Key": key},
127
+ ExpiresIn=min(S3_PRESIGNED_EXPIRY, 604800),
128
+ )
129
+ log.info(f"Uploaded to {STORAGE_PROVIDER} storage: s3://{S3_BUCKET}/{key}")
130
+ return url
131
+
132
+
133
+ def _cleanup(*paths: str):
134
+ for p in paths:
135
+ if p and os.path.exists(p):
136
+ try:
137
+ os.remove(p)
138
+ except OSError:
139
+ pass
140
+
141
+
142
+ class CompressRequest(BaseModel):
143
+ video_path: str = Field(..., description="Local path or URL to input video")
144
+ task_id: str = Field("", description="Task ID for logging")
145
+ codec: str = Field("AV1", description="Target codec: AV1, H264, HEVC, VP9")
146
+ codec_mode: str = Field("CRF", description="Rate control mode: CRF or VBR")
147
+ cq: int = Field(35, description="Constant quality value (lower = higher quality)")
148
+ preset: str = Field("p4", description="Encoder preset")
149
+ target_bitrate: Optional[int] = Field(None, description="Target bitrate in bps (for VBR mode)")
150
+ target_width: Optional[int] = Field(None, description="Target width for downscaling")
151
+ target_height: Optional[int] = Field(None, description="Target height for downscaling")
152
+
153
+
154
+ class CompressResponse(BaseModel):
155
+ output_path: str = Field("", description="Local path (local mode) or empty (remote mode)")
156
+ output_url: str = Field("", description="S3 presigned URL (remote mode) or empty (local mode)")
157
+ success: bool
158
+ error: Optional[str] = None
159
+ active_tasks: Optional[int] = None
160
+ queued_tasks: Optional[int] = None
161
+
162
+
163
+ @app.get("/health")
164
+ async def health():
165
+ return {
166
+ "status": "ok",
167
+ "max_concurrent": MAX_CONCURRENT,
168
+ "active_tasks": _active_count,
169
+ "queued_tasks": max(0, _queue_size - _active_count),
170
+ "storage": _storage_config_status(),
171
+ }
172
+
173
+
174
+ @app.get("/queue")
175
+ async def queue_status():
176
+ return {
177
+ "max_concurrent": MAX_CONCURRENT,
178
+ "active_tasks": _active_count,
179
+ "queued_tasks": max(0, _queue_size - _active_count),
180
+ "total_pending": _queue_size,
181
+ }
182
+
183
+
184
+ @app.post("/compress", response_model=CompressResponse)
185
+ async def compress(req: CompressRequest):
186
+ task_label = req.task_id or uuid.uuid4().hex[:8]
187
+ remote_mode = _is_url(req.video_path)
188
+ encoder = CODEC_MAP.get(req.codec.upper(), "av1_nvenc")
189
+
190
+ if remote_mode and DISABLE_REMOTE_IO:
191
+ return CompressResponse(success=False, error="Remote URL input is disabled for this local-only service")
192
+
193
+ os.makedirs(SHARED_VOLUME_PATH, exist_ok=True)
194
+
195
+ # --- Resolve input to local path ---
196
+ if remote_mode:
197
+ local_input = os.path.join(SHARED_VOLUME_PATH, f"{task_label}_input.mp4")
198
+ try:
199
+ await _download_url(req.video_path, local_input)
200
+ except Exception as e:
201
+ return CompressResponse(success=False, error=f"Failed to download input: {e}")
202
+ else:
203
+ local_input = req.video_path
204
+ if not os.path.exists(local_input):
205
+ raise HTTPException(status_code=400, detail=f"Input file not found: {local_input}")
206
+
207
+ basename = os.path.splitext(os.path.basename(local_input))[0]
208
+ output_filename = f"{basename}_compressed.mp4"
209
+ output_path = os.path.join(SHARED_VOLUME_PATH, output_filename)
210
+
211
+ # Build ffmpeg command
212
+ ffmpeg_args = [
213
+ "ffmpeg", "-y",
214
+ "-hwaccel", "cuda",
215
+ "-i", local_input,
216
+ ]
217
+
218
+ ffmpeg_args.extend(["-c:v", encoder])
219
+
220
+ if req.codec_mode == "VBR" and req.target_bitrate:
221
+ ffmpeg_args.extend(["-b:v", str(req.target_bitrate)])
222
+ else:
223
+ ffmpeg_args.extend(["-cq", str(req.cq)])
224
+
225
+ ffmpeg_args.extend(["-preset", req.preset])
226
+
227
+ if req.target_width and req.target_height:
228
+ w = req.target_width if req.target_width % 2 == 0 else req.target_width - 1
229
+ h = req.target_height if req.target_height % 2 == 0 else req.target_height - 1
230
+ ffmpeg_args.extend(["-vf", f"scale={w}:{h}"])
231
+
232
+ ffmpeg_args.extend(["-c:a", "copy"])
233
+ ffmpeg_args.append(output_path)
234
+
235
+ cmd = ffmpeg_args
236
+
237
+ global _queue_size, _active_count
238
+
239
+ async with _lock:
240
+ _queue_size += 1
241
+ queued = max(0, _queue_size - _active_count)
242
+
243
+ log.info(f"[{task_label}] Queued compression (codec={encoder}, cq={req.cq}, waiting={queued}, remote={remote_mode})")
244
+
245
+ try:
246
+ async with _semaphore:
247
+ async with _lock:
248
+ _active_count += 1
249
+ queued_now = max(0, _queue_size - _active_count)
250
+
251
+ log.info(f"[{task_label}] Starting compression (active={_active_count}/{MAX_CONCURRENT}, queued={queued_now})")
252
+ log.info(f"[{task_label}] Command: {' '.join(cmd)}")
253
+
254
+ proc = await asyncio.create_subprocess_exec(
255
+ *cmd,
256
+ stdout=asyncio.subprocess.PIPE,
257
+ stderr=asyncio.subprocess.PIPE,
258
+ )
259
+ stdout, stderr = await proc.communicate()
260
+
261
+ async with _lock:
262
+ _active_count -= 1
263
+ finally:
264
+ async with _lock:
265
+ _queue_size -= 1
266
+
267
+ stats = dict(active_tasks=_active_count, queued_tasks=max(0, _queue_size - _active_count))
268
+
269
+ if proc.returncode != 0:
270
+ err_msg = stderr.decode(errors="replace").strip()
271
+ log.error(f"[{task_label}] ffmpeg failed (rc={proc.returncode}): {err_msg}")
272
+ if remote_mode:
273
+ _cleanup(local_input)
274
+ return CompressResponse(success=False, error=err_msg, **stats)
275
+
276
+ if not os.path.exists(output_path):
277
+ log.error(f"[{task_label}] Output file not found: {output_path}")
278
+ if remote_mode:
279
+ _cleanup(local_input)
280
+ return CompressResponse(success=False, error="Output file not created", **stats)
281
+
282
+ # --- Remote mode: upload result to S3, return URL ---
283
+ if remote_mode:
284
+ try:
285
+ s3_key = f"processing/{task_label}/{output_filename}"
286
+ output_url = _upload_to_s3(output_path, s3_key)
287
+ log.info(f"[{task_label}] Compression complete (remote): {output_url[:80]}...")
288
+ return CompressResponse(output_url=output_url, success=True, **stats)
289
+ except Exception as e:
290
+ log.error(f"[{task_label}] S3 upload failed: {e}")
291
+ return CompressResponse(success=False, error=f"S3 upload failed: {e}", **stats)
292
+ finally:
293
+ _cleanup(local_input, output_path)
294
+
295
+ log.info(f"[{task_label}] Compression complete (local): {output_path}")
296
+ return CompressResponse(output_path=output_path, success=True, **stats)
miner/compression/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.115.12
2
+ uvicorn==0.34.2
3
+ pydantic==2.11.1
4
+ boto3==1.35.99
5
+ httpx==0.28.1
miner/docker-compose.yml ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ x-upscaling-video2x-base: &upscaling-video2x-base
2
+ build: ./upscaling
3
+ restart: unless-stopped
4
+ profiles: ["upscaling-video2x"]
5
+ networks:
6
+ - miner-local
7
+ environment:
8
+ - VIDEO2X_IMAGE=ghcr.io/k4yt3x/video2x:6.4.0
9
+ - SHARED_VOLUME_PATH=/tmp/organic-proxy
10
+ - HOST_SHARED_VOLUME_PATH=${MINER_SHARED_DIR:-/tmp/vidaio-miner-video-tmp}
11
+ - DOCKER_VOLUME_NAME=${COMPOSE_PROJECT_NAME:-organic-proxy}_video-tmp
12
+ - DISABLE_REMOTE_IO=true
13
+ - MAX_CONCURRENT_UPSCALING=2
14
+ - MINER_STORAGE_PROVIDER=${MINER_STORAGE_PROVIDER:-s3}
15
+ - MINER_STORAGE_S3_REGION=${MINER_STORAGE_S3_REGION:-us-east-1}
16
+ - MINER_STORAGE_S3_BUCKET_NAME=${MINER_STORAGE_S3_BUCKET_NAME:-}
17
+ - MINER_STORAGE_S3_ACCESS_KEY_ID=${MINER_STORAGE_S3_ACCESS_KEY_ID:-}
18
+ - MINER_STORAGE_S3_SECRET_ACCESS_KEY=${MINER_STORAGE_S3_SECRET_ACCESS_KEY:-}
19
+ - MINER_STORAGE_S3_ENDPOINT_URL=${MINER_STORAGE_S3_ENDPOINT_URL:-${MINER_STORAGE_S3_ENDPOINT:-}}
20
+ - MINER_STORAGE_S3_PRESIGNED_EXPIRY=${MINER_STORAGE_S3_PRESIGNED_EXPIRY:-${S3_PRESIGNED_EXPIRY:-3600}}
21
+ volumes:
22
+ - ${MINER_SHARED_DIR:-/tmp/vidaio-miner-video-tmp}:/tmp/organic-proxy
23
+ - /var/run/docker.sock:/var/run/docker.sock
24
+ healthcheck:
25
+ test: ["CMD", "curl", "-sf", "http://localhost:8003/health"]
26
+ interval: 30s
27
+ timeout: 10s
28
+ retries: 3
29
+ start_period: 10s
30
+
31
+ x-upscaling-ffmpeg-base: &upscaling-ffmpeg-base
32
+ build: ./upscaling/ffmpeg
33
+ restart: unless-stopped
34
+ profiles: ["upscaling-ffmpeg"]
35
+ gpus: all
36
+ networks:
37
+ - miner-local
38
+ environment:
39
+ - NVIDIA_VISIBLE_DEVICES=all
40
+ - NVIDIA_DRIVER_CAPABILITIES=video,compute,utility
41
+ - SHARED_VOLUME_PATH=/tmp/organic-proxy
42
+ - DISABLE_REMOTE_IO=true
43
+ - MAX_CONCURRENT_UPSCALING=2
44
+ - MINER_STORAGE_PROVIDER=${MINER_STORAGE_PROVIDER:-s3}
45
+ - MINER_STORAGE_S3_REGION=${MINER_STORAGE_S3_REGION:-us-east-1}
46
+ - MINER_STORAGE_S3_BUCKET_NAME=${MINER_STORAGE_S3_BUCKET_NAME:-}
47
+ - MINER_STORAGE_S3_ACCESS_KEY_ID=${MINER_STORAGE_S3_ACCESS_KEY_ID:-}
48
+ - MINER_STORAGE_S3_SECRET_ACCESS_KEY=${MINER_STORAGE_S3_SECRET_ACCESS_KEY:-}
49
+ - MINER_STORAGE_S3_ENDPOINT_URL=${MINER_STORAGE_S3_ENDPOINT_URL:-${MINER_STORAGE_S3_ENDPOINT:-}}
50
+ - MINER_STORAGE_S3_PRESIGNED_EXPIRY=${MINER_STORAGE_S3_PRESIGNED_EXPIRY:-${S3_PRESIGNED_EXPIRY:-3600}}
51
+ volumes:
52
+ - ${MINER_SHARED_DIR:-/tmp/vidaio-miner-video-tmp}:/tmp/organic-proxy
53
+ healthcheck:
54
+ test: ["CMD", "curl", "-sf", "http://localhost:8003/health"]
55
+ interval: 30s
56
+ timeout: 10s
57
+ retries: 3
58
+ start_period: 10s
59
+
60
+ x-compression-base: &compression-base
61
+ build: ./compression
62
+ restart: unless-stopped
63
+ gpus: all
64
+ networks:
65
+ - miner-local
66
+ environment:
67
+ - NVIDIA_VISIBLE_DEVICES=all
68
+ - NVIDIA_DRIVER_CAPABILITIES=video,compute,utility
69
+ - SHARED_VOLUME_PATH=/tmp/organic-proxy
70
+ - DISABLE_REMOTE_IO=true
71
+ - MAX_CONCURRENT_COMPRESSION=2
72
+ - MINER_STORAGE_PROVIDER=${MINER_STORAGE_PROVIDER:-s3}
73
+ - MINER_STORAGE_S3_REGION=${MINER_STORAGE_S3_REGION:-us-east-1}
74
+ - MINER_STORAGE_S3_BUCKET_NAME=${MINER_STORAGE_S3_BUCKET_NAME:-}
75
+ - MINER_STORAGE_S3_ACCESS_KEY_ID=${MINER_STORAGE_S3_ACCESS_KEY_ID:-}
76
+ - MINER_STORAGE_S3_SECRET_ACCESS_KEY=${MINER_STORAGE_S3_SECRET_ACCESS_KEY:-}
77
+ - MINER_STORAGE_S3_ENDPOINT_URL=${MINER_STORAGE_S3_ENDPOINT_URL:-${MINER_STORAGE_S3_ENDPOINT:-}}
78
+ - MINER_STORAGE_S3_PRESIGNED_EXPIRY=${MINER_STORAGE_S3_PRESIGNED_EXPIRY:-${S3_PRESIGNED_EXPIRY:-3600}}
79
+ volumes:
80
+ - ${MINER_SHARED_DIR:-/tmp/vidaio-miner-video-tmp}:/tmp/organic-proxy
81
+ healthcheck:
82
+ test: ["CMD", "curl", "-sf", "http://localhost:8004/health"]
83
+ interval: 30s
84
+ timeout: 10s
85
+ retries: 3
86
+ start_period: 10s
87
+
88
+ services:
89
+ upscaling-video2x:
90
+ <<: *upscaling-video2x-base
91
+ ports:
92
+ - "127.0.0.1:8003:8003"
93
+
94
+ upscaling-ffmpeg:
95
+ <<: *upscaling-ffmpeg-base
96
+ ports:
97
+ - "127.0.0.1:8005:8003"
98
+
99
+ compression:
100
+ <<: *compression-base
101
+ ports:
102
+ - "127.0.0.1:8004:8004"
103
+
104
+ networks:
105
+ miner-local:
106
+ driver: bridge
miner/requirements.txt ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Web Framework
2
+ fastapi==0.110.3
3
+ uvicorn==0.23.2
4
+ pydantic==2.4.2
5
+ # starlette==0.27.0
6
+
7
+ # Video Processing
8
+ ffmpeg-python==0.2.0
9
+ moviepy==1.0.3
10
+
11
+ # Storage
12
+ boto3==1.28.64
13
+ botocore==1.31.64
14
+ minio
15
+
16
+ # Task Management
17
+ redis==5.0.1
18
+ celery==5.3.4
19
+
20
+ # HTTP Client
21
+ httpx==0.25.0
22
+ firerequests
23
+
24
+ # Configuration
25
+ pyyaml==6.0.1
26
+ python-dotenv==1.0.0
27
+
28
+ # Utilities
29
+ # numpy==1.26.0
30
+ pillow==10.0.1
31
+ tqdm==4.66.1
32
+
33
+ # Logging
34
+ loguru==0.7.2
35
+ python-json-logger
36
+
37
+
38
+ # Testing
39
+ pytest==7.4.2
40
+ pytest-asyncio==0.21.1
41
+ pytest-mock==3.12.0
42
+
43
+ # Development
44
+ black==23.9.1
45
+ isort==5.12.0
46
+ flake8==6.1.0
47
+ sqlalchemy==2.0.23
48
+
49
+ # Bittensor
50
+ bittensor==9.9.0
51
+
52
+ rich
53
+ yarl==1.22.0
54
+
55
+ # Video hashing
56
+ opencv-python-headless>=4.8.0
57
+ numpy>=1.24.0
miner/upscaling/Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ RUN apt-get update && apt-get install -y --no-install-recommends \
4
+ curl \
5
+ ca-certificates \
6
+ gnupg \
7
+ lsb-release \
8
+ && install -m 0755 -d /etc/apt/keyrings \
9
+ && curl -fsSL https://download.docker.com/linux/debian/gpg \
10
+ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
11
+ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
12
+ https://download.docker.com/linux/debian $(lsb_release -cs) stable" \
13
+ > /etc/apt/sources.list.d/docker.list \
14
+ && apt-get update && apt-get install -y --no-install-recommends docker-ce-cli \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ WORKDIR /app
18
+
19
+ COPY requirements.txt .
20
+ RUN pip install --no-cache-dir -r requirements.txt
21
+
22
+ COPY app.py .
23
+
24
+ RUN mkdir -p /tmp/organic-proxy
25
+
26
+ EXPOSE 8003
27
+ CMD ["python3", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8003"]
miner/upscaling/SETUP.md ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```sh
2
+ # nvidia-container-toolkit
3
+ curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
4
+ curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
5
+ sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
6
+ sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
7
+ sudo apt update && sudo apt install -y nvidia-container-toolkit
8
+ sudo nvidia-ctk runtime configure --runtime=docker
9
+ sudo systemctl restart docker
10
+
11
+ # Pull video2x image
12
+ docker pull ghcr.io/k4yt3x/video2x:6.4.0
13
+ ```
14
+
15
+ The Video2X worker runs with `--network none` and `--pull never`, so this image must be present locally before starting the `upscaling-video2x` profile.
miner/upscaling/app.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Upscaling microservice โ€” wraps video2x (ghcr.io/k4yt3x/video2x:6.4.0).
3
+
4
+ Accepts a video path (on shared volume) OR a URL and scale factor (2 or 4),
5
+ runs video2x via Docker CLI, returns the output path or uploaded S3 URL.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import logging
12
+ import os
13
+ import subprocess
14
+ import uuid
15
+ from typing import Optional
16
+
17
+ import boto3
18
+ import httpx
19
+ from botocore.config import Config
20
+ from fastapi import FastAPI, HTTPException
21
+ from pydantic import BaseModel, Field
22
+
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format="%(asctime)s | %(levelname)-7s | %(message)s",
26
+ datefmt="%Y-%m-%d %H:%M:%S",
27
+ )
28
+ log = logging.getLogger("upscaling")
29
+
30
+ app = FastAPI(title="Video Upscaling Service")
31
+
32
+ VIDEO2X_IMAGE = os.getenv("VIDEO2X_IMAGE", "ghcr.io/k4yt3x/video2x:6.4.0")
33
+ SHARED_VOLUME_PATH = os.getenv("SHARED_VOLUME_PATH", "/tmp/organic-proxy")
34
+ DOCKER_VOLUME_NAME = os.getenv("DOCKER_VOLUME_NAME", "")
35
+ HOST_SHARED_VOLUME_PATH = os.getenv("HOST_SHARED_VOLUME_PATH", "").strip()
36
+ DISABLE_REMOTE_IO = os.getenv("DISABLE_REMOTE_IO", "false").lower() in ("1", "true", "yes")
37
+ MAX_CONCURRENT = int(os.getenv("MAX_CONCURRENT_UPSCALING", "2"))
38
+
39
+ # Storage provider label only. Uploads use one S3-compatible code path.
40
+ STORAGE_PROVIDER = os.getenv("MINER_STORAGE_PROVIDER", "s3").lower()
41
+ S3_REGION = os.getenv("MINER_STORAGE_S3_REGION", "us-east-1").strip() or "us-east-1"
42
+ S3_BUCKET = os.getenv("MINER_STORAGE_S3_BUCKET_NAME", "").strip()
43
+ S3_ACCESS_KEY_ID = os.getenv("MINER_STORAGE_S3_ACCESS_KEY_ID", "").strip()
44
+ S3_SECRET_ACCESS_KEY = os.getenv("MINER_STORAGE_S3_SECRET_ACCESS_KEY", "").strip()
45
+ S3_ENDPOINT_URL = (
46
+ os.getenv("MINER_STORAGE_S3_ENDPOINT_URL")
47
+ or os.getenv("MINER_STORAGE_S3_ENDPOINT")
48
+ or ""
49
+ ).strip()
50
+ S3_PRESIGNED_EXPIRY = int(
51
+ os.getenv("MINER_STORAGE_S3_PRESIGNED_EXPIRY")
52
+ or os.getenv("S3_PRESIGNED_EXPIRY")
53
+ or "3600"
54
+ )
55
+
56
+ _semaphore = asyncio.Semaphore(MAX_CONCURRENT)
57
+ _queue_size = 0
58
+ _active_count = 0
59
+ _lock = asyncio.Lock()
60
+
61
+
62
+ def _is_url(path: str) -> bool:
63
+ return path.startswith("http://") or path.startswith("https://")
64
+
65
+
66
+ def _get_s3_client():
67
+ client_kwargs = {
68
+ "region_name": S3_REGION,
69
+ "aws_access_key_id": S3_ACCESS_KEY_ID or None,
70
+ "aws_secret_access_key": S3_SECRET_ACCESS_KEY or None,
71
+ "config": Config(signature_version="s3v4"),
72
+ }
73
+ if S3_ENDPOINT_URL:
74
+ client_kwargs["endpoint_url"] = S3_ENDPOINT_URL
75
+ return boto3.client("s3", **client_kwargs)
76
+
77
+
78
+ def _storage_config_status() -> dict[str, object]:
79
+ return {
80
+ "provider": STORAGE_PROVIDER,
81
+ "region": S3_REGION,
82
+ "bucket_configured": bool(S3_BUCKET),
83
+ "access_key_configured": bool(S3_ACCESS_KEY_ID),
84
+ "secret_key_configured": bool(S3_SECRET_ACCESS_KEY),
85
+ "endpoint_configured": bool(S3_ENDPOINT_URL),
86
+ }
87
+
88
+
89
+ def _validate_s3_config():
90
+ missing = []
91
+ if not S3_BUCKET:
92
+ missing.append("MINER_STORAGE_S3_BUCKET_NAME")
93
+ if not S3_ACCESS_KEY_ID:
94
+ missing.append("MINER_STORAGE_S3_ACCESS_KEY_ID")
95
+ if not S3_SECRET_ACCESS_KEY:
96
+ missing.append("MINER_STORAGE_S3_SECRET_ACCESS_KEY")
97
+
98
+ if missing:
99
+ raise RuntimeError(f"Missing storage configuration: {', '.join(missing)}")
100
+
101
+
102
+ async def _download_url(url: str, dest: str):
103
+ """Download a URL (presigned S3 or direct) to local path."""
104
+ log.info(f"Downloading {url[:80]}... โ†’ {dest}")
105
+ async with httpx.AsyncClient(timeout=600.0, follow_redirects=True) as client:
106
+ async with client.stream("GET", url) as resp:
107
+ resp.raise_for_status()
108
+ with open(dest, "wb") as f:
109
+ async for chunk in resp.aiter_bytes(chunk_size=8192):
110
+ f.write(chunk)
111
+ log.info(f"Downloaded {os.path.getsize(dest) / (1024*1024):.1f} MB โ†’ {dest}")
112
+
113
+
114
+ def _upload_to_s3(local_path: str, key: str) -> str:
115
+ """Upload file to storage and return presigned URL."""
116
+ _validate_s3_config()
117
+ client = _get_s3_client()
118
+ client.upload_file(local_path, S3_BUCKET, key)
119
+ url = client.generate_presigned_url(
120
+ "get_object",
121
+ Params={"Bucket": S3_BUCKET, "Key": key},
122
+ ExpiresIn=min(S3_PRESIGNED_EXPIRY, 604800),
123
+ )
124
+ log.info(f"Uploaded to {STORAGE_PROVIDER} storage: s3://{S3_BUCKET}/{key}")
125
+ return url
126
+
127
+
128
+ class UpscaleRequest(BaseModel):
129
+ video_path: str = Field(..., description="Local path or URL to input video")
130
+ scale: int = Field(..., description="Upscaling factor: 2 or 4")
131
+ task_id: str = Field("", description="Task ID for logging")
132
+ model: str = Field("realesr-animevideov3", description="RealESRGAN model name")
133
+ codec: str = Field("hevc_nvenc", description="Output video codec")
134
+ cq: int = Field(35, description="Constant quality value")
135
+
136
+
137
+ class UpscaleResponse(BaseModel):
138
+ output_path: str = Field("", description="Local path (local mode) or empty (remote mode)")
139
+ output_url: str = Field("", description="S3 presigned URL (remote mode) or empty (local mode)")
140
+ success: bool
141
+ error: Optional[str] = None
142
+ queue_position: Optional[int] = None
143
+ active_tasks: Optional[int] = None
144
+ queued_tasks: Optional[int] = None
145
+
146
+
147
+ def _detect_volume_name() -> str:
148
+ if DOCKER_VOLUME_NAME:
149
+ return DOCKER_VOLUME_NAME
150
+ hostname = os.environ.get("HOSTNAME", "")
151
+ if hostname:
152
+ try:
153
+ result = subprocess.run(
154
+ ["docker", "inspect", "--format",
155
+ '{{range .Mounts}}{{if eq .Destination "' + SHARED_VOLUME_PATH + '"}}{{.Name}}{{end}}{{end}}',
156
+ hostname],
157
+ capture_output=True, text=True, timeout=10,
158
+ )
159
+ vol = result.stdout.strip()
160
+ if vol:
161
+ log.info(f"Auto-detected volume name: {vol}")
162
+ return vol
163
+ except Exception as e:
164
+ log.warning(f"Volume auto-detect failed: {e}")
165
+ fallback = "organic-proxy_video-tmp"
166
+ log.warning(f"Using fallback volume name: {fallback}")
167
+ return fallback
168
+
169
+
170
+ _resolved_volume: Optional[str] = None
171
+
172
+
173
+ def _get_volume_name() -> str:
174
+ global _resolved_volume
175
+ if _resolved_volume is None:
176
+ _resolved_volume = _detect_volume_name()
177
+ return _resolved_volume
178
+
179
+
180
+ def _get_docker_mount_source() -> str:
181
+ if HOST_SHARED_VOLUME_PATH:
182
+ return HOST_SHARED_VOLUME_PATH
183
+ return _get_volume_name()
184
+
185
+
186
+ @app.get("/health")
187
+ async def health():
188
+ return {
189
+ "status": "ok",
190
+ "max_concurrent": MAX_CONCURRENT,
191
+ "active_tasks": _active_count,
192
+ "queued_tasks": max(0, _queue_size - _active_count),
193
+ "storage": _storage_config_status(),
194
+ }
195
+
196
+
197
+ @app.get("/queue")
198
+ async def queue_status():
199
+ return {
200
+ "max_concurrent": MAX_CONCURRENT,
201
+ "active_tasks": _active_count,
202
+ "queued_tasks": max(0, _queue_size - _active_count),
203
+ "total_pending": _queue_size,
204
+ }
205
+
206
+
207
+ @app.post("/upscale", response_model=UpscaleResponse)
208
+ async def upscale(req: UpscaleRequest):
209
+ if req.scale not in (2, 4):
210
+ raise HTTPException(status_code=400, detail="scale must be 2 or 4")
211
+
212
+ task_label = req.task_id or uuid.uuid4().hex[:8]
213
+ remote_mode = _is_url(req.video_path)
214
+
215
+ if remote_mode and DISABLE_REMOTE_IO:
216
+ return UpscaleResponse(success=False, error="Remote URL input is disabled for this local-only service")
217
+
218
+ # --- Resolve input to local path ---
219
+ if remote_mode:
220
+ local_input = os.path.join(SHARED_VOLUME_PATH, f"{task_label}_input.mp4")
221
+ try:
222
+ await _download_url(req.video_path, local_input)
223
+ except Exception as e:
224
+ return UpscaleResponse(success=False, error=f"Failed to download input: {e}")
225
+ else:
226
+ local_input = req.video_path
227
+ if not os.path.exists(local_input):
228
+ raise HTTPException(status_code=400, detail=f"Input file not found: {local_input}")
229
+
230
+ basename = os.path.splitext(os.path.basename(local_input))[0]
231
+ output_filename = f"{basename}_upscaled_{req.scale}x.mp4"
232
+ output_path = os.path.join(SHARED_VOLUME_PATH, output_filename)
233
+
234
+ # Build paths relative to container mount point
235
+ rel_input = os.path.relpath(local_input, SHARED_VOLUME_PATH)
236
+ rel_output = os.path.relpath(output_path, SHARED_VOLUME_PATH)
237
+
238
+ cmd = [
239
+ "docker", "run",
240
+ "--gpus", "all",
241
+ "-e", "NVIDIA_DRIVER_CAPABILITIES=video,compute,utility",
242
+ "--network", "none",
243
+ "--pull", "never",
244
+ "--rm",
245
+ "-v", f"{_get_docker_mount_source()}:/host",
246
+ VIDEO2X_IMAGE,
247
+ "-i", f"/host/{rel_input}",
248
+ "-o", f"/host/{rel_output}",
249
+ "-p", "realesrgan",
250
+ "-s", str(req.scale),
251
+ "--realesrgan-model", req.model,
252
+ "-c", req.codec,
253
+ "-e", "preset=p7",
254
+ "-e", f"cq={req.cq}",
255
+ "-e", "profile=main",
256
+ "-e", "pix_fmt=yuv420p",
257
+ "-e", "sar=1:1",
258
+ "-e", "color_primaries=bt709",
259
+ "-e", "color_trc=bt709",
260
+ "-e", "colorspace=bt709",
261
+ "-e", "movflags=+faststart",
262
+ ]
263
+
264
+ global _queue_size, _active_count
265
+
266
+ async with _lock:
267
+ _queue_size += 1
268
+ position = _queue_size
269
+ queued = max(0, _queue_size - _active_count)
270
+
271
+ log.info(f"[{task_label}] Queued {req.scale}x upscale (position={position}, waiting={queued}, remote={remote_mode})")
272
+
273
+ try:
274
+ async with _semaphore:
275
+ async with _lock:
276
+ _active_count += 1
277
+ queued_now = max(0, _queue_size - _active_count)
278
+
279
+ log.info(f"[{task_label}] Starting {req.scale}x upscale (active={_active_count}/{MAX_CONCURRENT}, queued={queued_now})")
280
+ log.info(f"[{task_label}] Command: {' '.join(cmd)}")
281
+
282
+ proc = await asyncio.create_subprocess_exec(
283
+ *cmd,
284
+ stdout=asyncio.subprocess.PIPE,
285
+ stderr=asyncio.subprocess.PIPE,
286
+ )
287
+ stdout, stderr = await proc.communicate()
288
+
289
+ async with _lock:
290
+ _active_count -= 1
291
+ finally:
292
+ async with _lock:
293
+ _queue_size -= 1
294
+
295
+ stats = dict(active_tasks=_active_count, queued_tasks=max(0, _queue_size - _active_count))
296
+
297
+ if proc.returncode != 0:
298
+ err_msg = stderr.decode(errors="replace").strip()
299
+ log.error(f"[{task_label}] video2x failed (rc={proc.returncode}): {err_msg}")
300
+ if remote_mode:
301
+ _cleanup(local_input)
302
+ return UpscaleResponse(success=False, error=err_msg, **stats)
303
+
304
+ if not os.path.exists(output_path):
305
+ log.error(f"[{task_label}] Output file not found after video2x: {output_path}")
306
+ if remote_mode:
307
+ _cleanup(local_input)
308
+ return UpscaleResponse(success=False, error="Output file not created", **stats)
309
+
310
+ # --- Remote mode: upload result to S3, return URL ---
311
+ if remote_mode:
312
+ try:
313
+ s3_key = f"processing/{task_label}/{output_filename}"
314
+ output_url = _upload_to_s3(output_path, s3_key)
315
+ log.info(f"[{task_label}] Upscaling complete (remote): {output_url}")
316
+ return UpscaleResponse(output_url=output_url, success=True, **stats)
317
+ except Exception as e:
318
+ log.error(f"[{task_label}] S3 upload failed: {e}")
319
+ return UpscaleResponse(success=False, error=f"S3 upload failed: {e}", **stats)
320
+ finally:
321
+ _cleanup(local_input, output_path)
322
+
323
+ log.info(f"[{task_label}] Upscaling complete (local): {output_path}")
324
+ return UpscaleResponse(output_path=output_path, success=True, **stats)
325
+
326
+
327
+ def _cleanup(*paths: str):
328
+ for p in paths:
329
+ if p and os.path.exists(p):
330
+ try:
331
+ os.remove(p)
332
+ except OSError:
333
+ pass
miner/upscaling/ffmpeg/Dockerfile ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM nvidia/cuda:13.0.0-devel-ubuntu22.04
2
+
3
+ # Build dependencies for ffmpeg + NVENC/NVDEC + codec libraries
4
+ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
5
+ build-essential ninja-build nasm perl g++ cmake wget unzip \
6
+ nvidia-cuda-dev nvidia-cuda-toolkit pkg-config \
7
+ libx264-dev libx265-dev libvpx-dev \
8
+ git python3-pip python3-venv curl \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Install meson (needed for dav1d build)
12
+ RUN pip3 install --no-cache-dir meson
13
+
14
+ # nv-codec-headers (NVENC/NVDEC API headers)
15
+ RUN wget https://github.com/FFmpeg/nv-codec-headers/archive/876af32a202d0de83bd1d36fe74ee0f7fcf86b0d.zip \
16
+ && unzip 876af32a202d0de83bd1d36fe74ee0f7fcf86b0d.zip \
17
+ && cd nv-codec-headers-876af32a202d0de83bd1d36fe74ee0f7fcf86b0d \
18
+ && make && make install && cd .. \
19
+ && rm -rf nv-codec-headers-* 876af32a202d0de83bd1d36fe74ee0f7fcf86b0d.zip
20
+
21
+ # dav1d (AV1 decoder)
22
+ RUN git clone --depth 1 --branch 1.4.2 https://code.videolan.org/videolan/dav1d.git \
23
+ && cd dav1d && mkdir build && cd build \
24
+ && meson setup .. --default-library=static --buildtype release \
25
+ && ninja && ninja install && ldconfig \
26
+ && cd ../.. && rm -rf dav1d
27
+
28
+ # SVT-AV1 (AV1 encoder for libsvtav1)
29
+ RUN git clone --depth 1 --branch v2.3.0 https://gitlab.com/AOMediaCodec/SVT-AV1.git \
30
+ && cd SVT-AV1 && mkdir build && cd build \
31
+ && cmake .. -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \
32
+ && make -j$(nproc) && make install && ldconfig \
33
+ && cd ../.. && rm -rf SVT-AV1
34
+
35
+ # Build ffmpeg (pinned commit 33b215d) with CUDA + x264 + x265 + dav1d + svtav1
36
+ RUN export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:/usr/local/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH" \
37
+ && git init ffmpeg && cd ffmpeg \
38
+ && git remote add origin https://github.com/FFmpeg/FFmpeg.git \
39
+ && git fetch --depth 1 origin 33b215d1554a14e87416a24f8e6034312e629af7 \
40
+ && git checkout FETCH_HEAD \
41
+ && ./configure \
42
+ --enable-gpl \
43
+ --enable-libx264 \
44
+ --enable-libx265 \
45
+ --enable-libvpx \
46
+ --enable-libdav1d \
47
+ --enable-libsvtav1 \
48
+ --enable-nonfree \
49
+ --enable-cuda-nvcc \
50
+ --enable-nvenc \
51
+ --enable-nvdec \
52
+ --extra-cflags="-I/usr/local/cuda/include" \
53
+ --extra-ldflags="-L/usr/local/cuda/lib64" \
54
+ --disable-static \
55
+ --enable-shared \
56
+ --nvccflags="-gencode arch=compute_75,code=sm_75 -gencode arch=compute_75,code=compute_75 -O2" \
57
+ && make -j$(nproc) && make install && ldconfig \
58
+ && cd .. && rm -rf ffmpeg
59
+
60
+ WORKDIR /app
61
+ COPY requirements.txt .
62
+ RUN pip install --no-cache-dir -r requirements.txt
63
+ COPY app.py .
64
+ RUN mkdir -p /tmp/organic-proxy
65
+
66
+ EXPOSE 8003
67
+ CMD ["python3", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8003"]
miner/upscaling/ffmpeg/SETUP.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FFmpeg Upscaling Setup
2
+
3
+ This variant uses FFmpeg directly (CUDA/NVENC capable build) for video upscaling.
4
+
5
+ ## Notes
6
+ - The Dockerfile pins `nv-codec-headers`, `dav1d`, and FFmpeg commit `33b215d1554a14e87416a24f8e6034312e629af7`.
7
+ - Requires NVIDIA GPU runtime on the host.
8
+
9
+ ## Run
10
+ ```bash
11
+ docker compose --profile upscaling-ffmpeg up -d upscaling-ffmpeg
12
+ ```
miner/upscaling/ffmpeg/app.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ import subprocess
7
+ import uuid
8
+ from typing import Optional
9
+
10
+ import boto3
11
+ import httpx
12
+ from botocore.config import Config
13
+ from fastapi import FastAPI, HTTPException
14
+ from pydantic import BaseModel, Field
15
+
16
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)-7s | %(message)s")
17
+ log = logging.getLogger("upscaling-ffmpeg")
18
+
19
+ app = FastAPI(title="FFmpeg Upscaling Service")
20
+
21
+ SHARED_VOLUME_PATH = os.getenv("SHARED_VOLUME_PATH", "/tmp/organic-proxy")
22
+ MAX_CONCURRENT = int(os.getenv("MAX_CONCURRENT_UPSCALING", "2"))
23
+ FFMPEG_CODEC = os.getenv("FFMPEG_CODEC", "hevc_nvenc")
24
+ FFMPEG_PRESET = os.getenv("FFMPEG_PRESET", "p7")
25
+ FFMPEG_CQ = int(os.getenv("FFMPEG_CQ", "35"))
26
+ DISABLE_REMOTE_IO = os.getenv("DISABLE_REMOTE_IO", "false").lower() in ("1", "true", "yes")
27
+
28
+ STORAGE_PROVIDER = os.getenv("MINER_STORAGE_PROVIDER", "s3").lower()
29
+ S3_REGION = os.getenv("MINER_STORAGE_S3_REGION", "us-east-1").strip() or "us-east-1"
30
+ S3_BUCKET = os.getenv("MINER_STORAGE_S3_BUCKET_NAME", "").strip()
31
+ S3_ACCESS_KEY_ID = os.getenv("MINER_STORAGE_S3_ACCESS_KEY_ID", "").strip()
32
+ S3_SECRET_ACCESS_KEY = os.getenv("MINER_STORAGE_S3_SECRET_ACCESS_KEY", "").strip()
33
+ S3_ENDPOINT_URL = (os.getenv("MINER_STORAGE_S3_ENDPOINT_URL") or os.getenv("MINER_STORAGE_S3_ENDPOINT") or "").strip()
34
+ S3_PRESIGNED_EXPIRY = int(os.getenv("MINER_STORAGE_S3_PRESIGNED_EXPIRY") or os.getenv("S3_PRESIGNED_EXPIRY") or "3600")
35
+
36
+ _semaphore = asyncio.Semaphore(MAX_CONCURRENT)
37
+ _queue_size = 0
38
+ _active_count = 0
39
+ _lock = asyncio.Lock()
40
+
41
+
42
+ def _is_url(path: str) -> bool:
43
+ return path.startswith("http://") or path.startswith("https://")
44
+
45
+
46
+ def _get_s3_client():
47
+ kwargs = {
48
+ "region_name": S3_REGION,
49
+ "aws_access_key_id": S3_ACCESS_KEY_ID or None,
50
+ "aws_secret_access_key": S3_SECRET_ACCESS_KEY or None,
51
+ "config": Config(signature_version="s3v4"),
52
+ }
53
+ if S3_ENDPOINT_URL:
54
+ kwargs["endpoint_url"] = S3_ENDPOINT_URL
55
+ return boto3.client("s3", **kwargs)
56
+
57
+
58
+ def _validate_s3_config():
59
+ missing = [k for k, v in {
60
+ "MINER_STORAGE_S3_BUCKET_NAME": S3_BUCKET,
61
+ "MINER_STORAGE_S3_ACCESS_KEY_ID": S3_ACCESS_KEY_ID,
62
+ "MINER_STORAGE_S3_SECRET_ACCESS_KEY": S3_SECRET_ACCESS_KEY,
63
+ }.items() if not v]
64
+ if missing:
65
+ raise RuntimeError(f"Missing storage configuration: {', '.join(missing)}")
66
+
67
+
68
+ async def _download_url(url: str, dest: str):
69
+ async with httpx.AsyncClient(timeout=600.0, follow_redirects=True) as client:
70
+ async with client.stream("GET", url) as resp:
71
+ resp.raise_for_status()
72
+ with open(dest, "wb") as f:
73
+ async for chunk in resp.aiter_bytes(chunk_size=8192):
74
+ f.write(chunk)
75
+
76
+
77
+ def _upload_to_s3(local_path: str, key: str) -> str:
78
+ _validate_s3_config()
79
+ client = _get_s3_client()
80
+ client.upload_file(local_path, S3_BUCKET, key)
81
+ return client.generate_presigned_url("get_object", Params={"Bucket": S3_BUCKET, "Key": key}, ExpiresIn=min(S3_PRESIGNED_EXPIRY, 604800))
82
+
83
+
84
+ class UpscaleRequest(BaseModel):
85
+ video_path: str
86
+ scale: int = Field(..., description="Upscaling factor: 2 or 4")
87
+ task_id: str = ""
88
+
89
+
90
+ class UpscaleResponse(BaseModel):
91
+ output_path: str = ""
92
+ output_url: str = ""
93
+ success: bool
94
+ error: Optional[str] = None
95
+
96
+
97
+ @app.get("/health")
98
+ async def health():
99
+ return {"status": "ok", "max_concurrent": MAX_CONCURRENT, "active_tasks": _active_count, "queued_tasks": max(0, _queue_size - _active_count)}
100
+
101
+
102
+ @app.post("/upscale", response_model=UpscaleResponse)
103
+ async def upscale(req: UpscaleRequest):
104
+ global _queue_size, _active_count
105
+
106
+ if req.scale not in (2, 4):
107
+ raise HTTPException(status_code=400, detail="scale must be 2 or 4")
108
+
109
+ task = req.task_id or uuid.uuid4().hex[:8]
110
+ remote = _is_url(req.video_path)
111
+ if remote and DISABLE_REMOTE_IO:
112
+ return UpscaleResponse(success=False, error="Remote URL input is disabled for this local-only service")
113
+
114
+ local_input = os.path.join(SHARED_VOLUME_PATH, f"{task}_input.mp4") if remote else req.video_path
115
+ if remote:
116
+ await _download_url(req.video_path, local_input)
117
+ elif not os.path.exists(local_input):
118
+ raise HTTPException(status_code=400, detail=f"Input file not found: {local_input}")
119
+
120
+ base = os.path.splitext(os.path.basename(local_input))[0]
121
+ output_path = os.path.join(SHARED_VOLUME_PATH, f"{base}_upscaled_{req.scale}x.mp4")
122
+
123
+ cmd = [
124
+ "ffmpeg", "-y", "-hwaccel", "cuda", "-i", local_input,
125
+ "-vf", f"scale=iw*{req.scale}:ih*{req.scale}:flags=lanczos",
126
+ "-c:v", FFMPEG_CODEC, "-preset", FFMPEG_PRESET, "-cq", str(FFMPEG_CQ),
127
+ "-c:a", "copy", output_path,
128
+ ]
129
+
130
+ async with _lock:
131
+ _queue_size += 1
132
+ async with _semaphore:
133
+ async with _lock:
134
+ _active_count += 1
135
+ try:
136
+ result = await asyncio.to_thread(subprocess.run, cmd, capture_output=True, text=True)
137
+ if result.returncode != 0:
138
+ return UpscaleResponse(success=False, error=result.stderr[-1000:])
139
+ if remote:
140
+ key = f"upscaling/{task}/{os.path.basename(output_path)}"
141
+ url = _upload_to_s3(output_path, key)
142
+ return UpscaleResponse(success=True, output_url=url)
143
+ return UpscaleResponse(success=True, output_path=output_path)
144
+ finally:
145
+ async with _lock:
146
+ _active_count = max(0, _active_count - 1)
147
+ _queue_size = max(0, _queue_size - 1)
miner/upscaling/ffmpeg/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn==0.30.1
3
+ boto3==1.34.122
4
+ httpx==0.27.0
5
+ pydantic==2.8.2
miner/upscaling/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.115.12
2
+ uvicorn==0.34.2
3
+ pydantic==2.11.1
4
+ boto3==1.35.99
5
+ httpx==0.28.1
neurons/miner.py CHANGED
@@ -1,11 +1,18 @@
 
 
 
1
  import time
2
- import uuid
3
  import traceback
4
- import os
5
- import asyncio
6
  from typing import Tuple
7
- from loguru import logger
 
 
8
  import bittensor as bt
 
 
 
9
  from vidaio_subnet_core.base.miner import BaseMiner
10
  from vidaio_subnet_core.protocol import (
11
  VideoUpscalingProtocol,
@@ -21,13 +28,58 @@ from vidaio_subnet_core.protocol import (
21
  JobKickoffResponse,
22
  PollResponse,
23
  )
24
- from services.miner_utilities.miner_utils import video_upscaler, video_compressor
25
 
26
  from vidaio_subnet_core.utilities.version import check_version
27
 
 
 
 
28
  MAX_CONTENT_LEN = ContentLength.FIVE
29
  warrant_task = TaskType.UPSCALING
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  class Miner(BaseMiner):
32
  def __init__(self, config: dict | None = None) -> None:
33
  """
@@ -38,6 +90,219 @@ class Miner(BaseMiner):
38
  # Structure: { job_id: {"task": asyncio.Task, "result": str | None, "error": str | None} }
39
  self._jobs: dict[str, dict] = {}
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  async def forward_upscaling_requests(self, synapse: VideoUpscalingProtocol) -> VideoUpscalingProtocol:
42
  """
43
  Processes a video upscaling request by downloading, upscaling,
@@ -55,7 +320,7 @@ class Miner(BaseMiner):
55
  check_version(synapse.version)
56
 
57
  try:
58
- processed_video_url = await video_upscaler(payload_url, task_type)
59
 
60
  if processed_video_url is None:
61
  logger.info(f"๐Ÿ’” Failed to upscaling video ๐Ÿ’”")
@@ -82,7 +347,6 @@ class Miner(BaseMiner):
82
 
83
  start_time = time.time()
84
 
85
- payload_url: str = synapse.miner_payload.reference_video_url
86
  vmaf_threshold: float = synapse.miner_payload.vmaf_threshold
87
  target_codec: str = synapse.miner_payload.target_codec
88
  codec_mode: str = synapse.miner_payload.codec_mode
@@ -94,7 +358,7 @@ class Miner(BaseMiner):
94
  check_version(synapse.version)
95
 
96
  try:
97
- processed_video_url = await video_compressor(payload_url, vmaf_threshold, target_codec, codec_mode, target_bitrate)
98
 
99
  if processed_video_url is None:
100
  logger.info(f"๐Ÿ’” Failed to compress video ๐Ÿ’”")
@@ -159,16 +423,10 @@ class Miner(BaseMiner):
159
  synapse.job_response = JobKickoffResponse(accepted=False)
160
  return synapse
161
 
162
- payload_url = synapse.miner_payload.reference_video_url
163
- vmaf_threshold = synapse.miner_payload.vmaf_threshold
164
- target_codec = synapse.miner_payload.target_codec
165
- codec_mode = synapse.miner_payload.codec_mode
166
- target_bitrate = synapse.miner_payload.target_bitrate
167
-
168
  async def _run_compression():
169
- return await video_compressor(
170
- payload_url, vmaf_threshold, target_codec, codec_mode, target_bitrate
171
- )
172
 
173
  task = asyncio.create_task(_run_compression())
174
  self._jobs[job_id] = {"task": task, "result": None, "error": None}
@@ -275,7 +533,9 @@ class Miner(BaseMiner):
275
  task_type = synapse.miner_payload.task_type
276
 
277
  async def _run_upscaling():
278
- return await video_upscaler(payload_url, task_type)
 
 
279
 
280
  task = asyncio.create_task(_run_upscaling())
281
  self._jobs[job_id] = {"task": task, "result": None, "error": None}
 
1
+ import asyncio
2
+ import os
3
+ import posixpath
4
  import time
 
5
  import traceback
6
+ import uuid
7
+ from pathlib import Path
8
  from typing import Tuple
9
+
10
+ import boto3
11
+ import httpx
12
  import bittensor as bt
13
+ from botocore.config import Config as BotoConfig
14
+ from dotenv import load_dotenv
15
+ from loguru import logger
16
  from vidaio_subnet_core.base.miner import BaseMiner
17
  from vidaio_subnet_core.protocol import (
18
  VideoUpscalingProtocol,
 
28
  JobKickoffResponse,
29
  PollResponse,
30
  )
 
31
 
32
  from vidaio_subnet_core.utilities.version import check_version
33
 
34
+ REPO_ROOT = Path(__file__).resolve().parents[1]
35
+ load_dotenv(REPO_ROOT / "miner" / ".env", override=False)
36
+
37
  MAX_CONTENT_LEN = ContentLength.FIVE
38
  warrant_task = TaskType.UPSCALING
39
 
40
+ UPSCALING_SERVICE_URL = os.getenv("MINER_UPSCALING_SERVICE_URL", "http://localhost:8003").rstrip("/")
41
+ COMPRESSION_SERVICE_URL = os.getenv("MINER_COMPRESSION_SERVICE_URL", "http://localhost:8004").rstrip("/")
42
+ UPSCALING_SERVICE_TIMEOUT_SECONDS = float(os.getenv("MINER_UPSCALING_SERVICE_TIMEOUT_SECONDS", "1200"))
43
+ COMPRESSION_SERVICE_TIMEOUT_SECONDS = float(os.getenv("MINER_COMPRESSION_SERVICE_TIMEOUT_SECONDS", "600"))
44
+ MINER_DOWNLOAD_TIMEOUT_SECONDS = float(os.getenv("MINER_DOWNLOAD_TIMEOUT_SECONDS", "600"))
45
+ MINER_UPLOAD_URL_EXPIRY_SECONDS = int(os.getenv("MINER_STORAGE_S3_PRESIGNED_EXPIRY", os.getenv("S3_PRESIGNED_EXPIRY", "3600")))
46
+
47
+ HOST_SHARED_VOLUME_PATH = Path(
48
+ os.getenv("MINER_SHARED_VOLUME_PATH")
49
+ or os.getenv("MINER_SHARED_DIR", "/tmp/vidaio-miner-video-tmp")
50
+ ).expanduser()
51
+ if not HOST_SHARED_VOLUME_PATH.is_absolute():
52
+ HOST_SHARED_VOLUME_PATH = REPO_ROOT / HOST_SHARED_VOLUME_PATH
53
+ CONTAINER_SHARED_VOLUME_PATH = os.getenv("MINER_CONTAINER_SHARED_VOLUME_PATH", "/tmp/organic-proxy").rstrip("/")
54
+
55
+ S3_REGION = os.getenv("MINER_STORAGE_S3_REGION", "us-east-1").strip() or "us-east-1"
56
+ S3_BUCKET = os.getenv("MINER_STORAGE_S3_BUCKET_NAME", "").strip()
57
+ S3_ACCESS_KEY_ID = os.getenv("MINER_STORAGE_S3_ACCESS_KEY_ID", "").strip()
58
+ S3_SECRET_ACCESS_KEY = os.getenv("MINER_STORAGE_S3_SECRET_ACCESS_KEY", "").strip()
59
+ S3_ENDPOINT_URL = (
60
+ os.getenv("MINER_STORAGE_S3_ENDPOINT_URL")
61
+ or os.getenv("MINER_STORAGE_S3_ENDPOINT")
62
+ or ""
63
+ ).strip()
64
+
65
+ TASK_TYPE_TO_SCALE = {
66
+ "SD24K": 4,
67
+ "HD24K": 2,
68
+ "SD2HD": 2,
69
+ "4K28K": 2,
70
+ }
71
+
72
+ COMPRESSION_CQ_BY_TYPE = {
73
+ "Low": 40,
74
+ "Medium": 35,
75
+ "High": 30,
76
+ }
77
+
78
+
79
+ def _is_url(path: str) -> bool:
80
+ return path.startswith("http://") or path.startswith("https://")
81
+
82
+
83
  class Miner(BaseMiner):
84
  def __init__(self, config: dict | None = None) -> None:
85
  """
 
90
  # Structure: { job_id: {"task": asyncio.Task, "result": str | None, "error": str | None} }
91
  self._jobs: dict[str, dict] = {}
92
 
93
+ def _container_shared_path(self, host_path: Path) -> str:
94
+ return f"{CONTAINER_SHARED_VOLUME_PATH}/{host_path.name}"
95
+
96
+ def _host_shared_path(self, container_path: str) -> Path:
97
+ shared_mount = posixpath.normpath(CONTAINER_SHARED_VOLUME_PATH)
98
+ normalized_path = posixpath.normpath(container_path)
99
+
100
+ if normalized_path == shared_mount or not normalized_path.startswith(f"{shared_mount}/"):
101
+ raise ValueError(f"Service returned path outside shared volume: {container_path}")
102
+
103
+ relative_path = posixpath.relpath(normalized_path, shared_mount)
104
+ host_path = (HOST_SHARED_VOLUME_PATH / relative_path).resolve()
105
+ shared_root = HOST_SHARED_VOLUME_PATH.resolve()
106
+
107
+ try:
108
+ host_path.relative_to(shared_root)
109
+ except ValueError:
110
+ raise ValueError(f"Mapped host path escapes shared volume: {host_path}") from None
111
+
112
+ return host_path
113
+
114
+ async def _download_to_shared_volume(self, video_url: str, task_id: str) -> tuple[Path, str]:
115
+ HOST_SHARED_VOLUME_PATH.mkdir(parents=True, exist_ok=True)
116
+ host_path = HOST_SHARED_VOLUME_PATH / f"{task_id}_input.mp4"
117
+
118
+ logger.info(f"Downloading validator payload to shared volume: {host_path}")
119
+ timeout = httpx.Timeout(MINER_DOWNLOAD_TIMEOUT_SECONDS)
120
+ try:
121
+ async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
122
+ async with client.stream("GET", video_url) as response:
123
+ response.raise_for_status()
124
+ with open(host_path, "wb") as file:
125
+ async for chunk in response.aiter_bytes(chunk_size=1024 * 1024):
126
+ file.write(chunk)
127
+ except Exception:
128
+ self._cleanup_shared_files(host_path)
129
+ raise
130
+
131
+ return host_path, self._container_shared_path(host_path)
132
+
133
+ def _get_s3_client(self):
134
+ client_kwargs = {
135
+ "region_name": S3_REGION,
136
+ "aws_access_key_id": S3_ACCESS_KEY_ID or None,
137
+ "aws_secret_access_key": S3_SECRET_ACCESS_KEY or None,
138
+ "config": BotoConfig(signature_version="s3v4"),
139
+ }
140
+ if S3_ENDPOINT_URL:
141
+ client_kwargs["endpoint_url"] = S3_ENDPOINT_URL
142
+ return boto3.client("s3", **client_kwargs)
143
+
144
+ async def _upload_processed_video(self, host_path: Path, service_name: str, task_id: str) -> str | None:
145
+ missing = []
146
+ if not S3_BUCKET:
147
+ missing.append("MINER_STORAGE_S3_BUCKET_NAME")
148
+ if not S3_ACCESS_KEY_ID:
149
+ missing.append("MINER_STORAGE_S3_ACCESS_KEY_ID")
150
+ if not S3_SECRET_ACCESS_KEY:
151
+ missing.append("MINER_STORAGE_S3_SECRET_ACCESS_KEY")
152
+
153
+ if missing:
154
+ logger.error(f"Missing storage configuration: {', '.join(missing)}")
155
+ return None
156
+
157
+ if not host_path.exists():
158
+ logger.error(f"Processed file not found on shared volume: {host_path}")
159
+ return None
160
+
161
+ object_key = f"processing/{service_name}/{task_id}/{host_path.name}"
162
+
163
+ def _upload_and_sign() -> str:
164
+ client = self._get_s3_client()
165
+ client.upload_file(str(host_path), S3_BUCKET, object_key)
166
+ return client.generate_presigned_url(
167
+ "get_object",
168
+ Params={"Bucket": S3_BUCKET, "Key": object_key},
169
+ ExpiresIn=min(MINER_UPLOAD_URL_EXPIRY_SECONDS, 604800),
170
+ )
171
+
172
+ logger.info(f"Uploading processed {service_name} output: {object_key}")
173
+ return await asyncio.to_thread(_upload_and_sign)
174
+
175
+ def _cleanup_shared_files(self, *paths: Path | None):
176
+ for path in paths:
177
+ if path and path.exists():
178
+ try:
179
+ path.unlink()
180
+ except OSError as e:
181
+ logger.warning(f"Failed to remove shared file {path}: {e}")
182
+
183
+ def _upscaling_service_payload(self, video_path: str, task_type: str, task_id: str) -> dict:
184
+ scale = TASK_TYPE_TO_SCALE.get(task_type, 2)
185
+ if task_type not in TASK_TYPE_TO_SCALE:
186
+ logger.warning(f"Unknown upscaling task_type={task_type}, defaulting to 2x scale")
187
+
188
+ return {
189
+ "video_path": video_path,
190
+ "scale": scale,
191
+ "task_id": task_id,
192
+ }
193
+
194
+ def _compression_type_from_payload(self, payload) -> str:
195
+ task_data = payload.model_dump() if hasattr(payload, "model_dump") else {}
196
+ compression_type = task_data.get("compression_type") or getattr(payload, "compression_type", None)
197
+ if compression_type in COMPRESSION_CQ_BY_TYPE:
198
+ return compression_type
199
+
200
+ vmaf_threshold = float(payload.vmaf_threshold)
201
+ if vmaf_threshold >= 93:
202
+ return "High"
203
+ if vmaf_threshold >= 89:
204
+ return "Medium"
205
+ return "Low"
206
+
207
+ def _compression_service_payload(self, payload, video_path: str, task_id: str) -> dict:
208
+ compression_type = self._compression_type_from_payload(payload)
209
+ compression_config = {
210
+ "video_path": video_path,
211
+ "task_id": task_id,
212
+ "codec": payload.target_codec,
213
+ "codec_mode": payload.codec_mode.upper(),
214
+ "target_bitrate": int(float(payload.target_bitrate) * 1_000_000),
215
+ }
216
+
217
+ if compression_type == "Low":
218
+ compression_config["cq"] = 40
219
+ elif compression_type == "Medium":
220
+ compression_config["cq"] = 35
221
+ elif compression_type == "High":
222
+ compression_config["cq"] = 30
223
+
224
+ return compression_config
225
+
226
+ async def _post_processing_service(
227
+ self,
228
+ service_name: str,
229
+ service_url: str,
230
+ endpoint: str,
231
+ payload: dict,
232
+ timeout_seconds: float,
233
+ ) -> str | None:
234
+ url = f"{service_url}{endpoint}"
235
+ timeout = httpx.Timeout(timeout_seconds)
236
+
237
+ logger.info(f"Forwarding {service_name} request to {url}")
238
+ async with httpx.AsyncClient(timeout=timeout) as client:
239
+ response = await client.post(url, json=payload)
240
+
241
+ if response.status_code != 200:
242
+ logger.error(f"{service_name} service error {response.status_code}: {response.text[:1000]}")
243
+ return None
244
+
245
+ result = response.json()
246
+ if not result.get("success", False):
247
+ logger.error(f"{service_name} service failed: {result.get('error')}")
248
+ return None
249
+
250
+ processed_video_url = result.get("output_url") or result.get("output_path")
251
+ if not processed_video_url:
252
+ logger.error(f"{service_name} service returned no output URL/path")
253
+ return None
254
+
255
+ logger.info(f"Received {service_name} result from container service")
256
+ return processed_video_url
257
+
258
+ async def _forward_upscaling_to_service(self, payload_url: str, task_type: str) -> str | None:
259
+ task_id = uuid.uuid4().hex[:12]
260
+ input_host_path = None
261
+ output_host_path = None
262
+ try:
263
+ input_host_path, input_container_path = await self._download_to_shared_volume(payload_url, task_id)
264
+ processed_ref = await self._post_processing_service(
265
+ "upscaling",
266
+ UPSCALING_SERVICE_URL,
267
+ "/upscale",
268
+ self._upscaling_service_payload(input_container_path, task_type, task_id),
269
+ UPSCALING_SERVICE_TIMEOUT_SECONDS,
270
+ )
271
+ if processed_ref is None:
272
+ return None
273
+ if _is_url(processed_ref):
274
+ return processed_ref
275
+
276
+ output_host_path = self._host_shared_path(processed_ref)
277
+ return await self._upload_processed_video(output_host_path, "upscaling", task_id)
278
+ finally:
279
+ self._cleanup_shared_files(input_host_path, output_host_path)
280
+
281
+ async def _forward_compression_to_service(self, payload) -> str | None:
282
+ task_id = uuid.uuid4().hex[:12]
283
+ input_host_path = None
284
+ output_host_path = None
285
+ try:
286
+ input_host_path, input_container_path = await self._download_to_shared_volume(
287
+ payload.reference_video_url, task_id
288
+ )
289
+ processed_ref = await self._post_processing_service(
290
+ "compression",
291
+ COMPRESSION_SERVICE_URL,
292
+ "/compress",
293
+ self._compression_service_payload(payload, input_container_path, task_id),
294
+ COMPRESSION_SERVICE_TIMEOUT_SECONDS,
295
+ )
296
+ if processed_ref is None:
297
+ return None
298
+ if _is_url(processed_ref):
299
+ return processed_ref
300
+
301
+ output_host_path = self._host_shared_path(processed_ref)
302
+ return await self._upload_processed_video(output_host_path, "compression", task_id)
303
+ finally:
304
+ self._cleanup_shared_files(input_host_path, output_host_path)
305
+
306
  async def forward_upscaling_requests(self, synapse: VideoUpscalingProtocol) -> VideoUpscalingProtocol:
307
  """
308
  Processes a video upscaling request by downloading, upscaling,
 
320
  check_version(synapse.version)
321
 
322
  try:
323
+ processed_video_url = await self._forward_upscaling_to_service(payload_url, task_type)
324
 
325
  if processed_video_url is None:
326
  logger.info(f"๐Ÿ’” Failed to upscaling video ๐Ÿ’”")
 
347
 
348
  start_time = time.time()
349
 
 
350
  vmaf_threshold: float = synapse.miner_payload.vmaf_threshold
351
  target_codec: str = synapse.miner_payload.target_codec
352
  codec_mode: str = synapse.miner_payload.codec_mode
 
358
  check_version(synapse.version)
359
 
360
  try:
361
+ processed_video_url = await self._forward_compression_to_service(synapse.miner_payload)
362
 
363
  if processed_video_url is None:
364
  logger.info(f"๐Ÿ’” Failed to compress video ๐Ÿ’”")
 
423
  synapse.job_response = JobKickoffResponse(accepted=False)
424
  return synapse
425
 
 
 
 
 
 
 
426
  async def _run_compression():
427
+ result = await self._forward_compression_to_service(synapse.miner_payload)
428
+ logger.info(f"CompressionJob processed successfully | job_id={job_id} | result={result}")
429
+ return result
430
 
431
  task = asyncio.create_task(_run_compression())
432
  self._jobs[job_id] = {"task": task, "result": None, "error": None}
 
533
  task_type = synapse.miner_payload.task_type
534
 
535
  async def _run_upscaling():
536
+ result = await self._forward_upscaling_to_service(payload_url, task_type)
537
+ logger.info(f"UpscalingJob processed successfully | job_id={job_id} | result={result}")
538
+ return result
539
 
540
  task = asyncio.create_task(_run_upscaling())
541
  self._jobs[job_id] = {"task": task, "result": None, "error": None}