# FFmpeg Processing API

Base URL:

```text
https://loudness-api.jamrockdev.com
```

No authentication is currently required.

## Endpoint: Measure Loudness

```http
POST /measure-loudness
Content-Type: application/json
```

### Request Body

```json
{
  "audio_url": "https://example.com/path/to/audio-file.mp3"
}
```

`audio_url` is required and must be a publicly accessible `http` or `https`
URL. Any format FFmpeg can decode is supported, including MP3, WAV, M4A, FLAC,
OGG, MP4, and WebM.

### Behavior

The API runs this FFmpeg command synchronously:

```bash
ffmpeg -i "<audio_url>" -af loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json -f null -
```

It parses the loudnorm JSON printed to stderr and returns numeric values. The
main value is `input_i`, the integrated loudness of the input in LUFS.

### Success Response

HTTP `200`

```json
{
  "success": true,
  "input_i": -24.52,
  "input_tp": -2.04,
  "input_lra": 7.3,
  "input_thresh": -34.82,
  "output_i": -16.0,
  "output_tp": -1.5,
  "output_lra": 5.5,
  "output_thresh": -26.2,
  "normalization_type": "dynamic",
  "target_offset": 0.0
}
```

## Endpoint: Run FFmpeg Command

```http
POST /run-ffmpeg-command
Content-Type: application/json
```

### Request Body

```json
{
  "input_files": {
    "in_1": "https://example.com/input-audio-or-video.webm",
    "in_2": "https://example.com/optional-second-input.mp3",
    "in_3": "https://example.com/optional-third-input.mp3"
  },
  "output_files": {
    "out_1": "VOCALS_abc123.mp3"
  },
  "ffmpeg_command": "-i {{in_1}} -vn -acodec libmp3lame -q:a 2 {{out_1}}",
  "webhook_url": "https://fvaaxuhjofhbngkalqcv.supabase.co/functions/v1/receive-vocals",
  "webhook_metadata": {
    "recording_id": "abc123"
  }
}
```

### Field Definitions

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `input_files` | object | Yes | Key-value pairs of input labels to public URLs. Labels must be `in_1`, `in_2`, `in_3`, etc. |
| `output_files` | object | Yes | Key-value pairs of output labels to filenames. Labels must be `out_1`, `out_2`, etc. Filenames must not contain path separators. |
| `ffmpeg_command` | string | Yes | FFmpeg arguments with `{{in_1}}`, `{{in_2}}`, `{{out_1}}` placeholders. Do not include shell syntax. Including a leading `ffmpeg` is accepted but not required. |
| `webhook_url` | string | Yes | Must be an allowed receive-vocals webhook URL. Primary: `https://fvaaxuhjofhbngkalqcv.supabase.co/functions/v1/receive-vocals`. Legacy allowed: `https://sxlwnmfsiahrqxfpkqmz.supabase.co/functions/v1/receive-vocals`. |
| `webhook_metadata` | object | Yes | Arbitrary JSON object passed through to the webhook unchanged. |

### Immediate Response

The server accepts the request, queues processing, and immediately returns:

```json
{
  "command_id": "job-unique-id"
}
```

### Asynchronous Behavior

The server then:

1. Downloads every `input_files` URL into local temp storage.
2. Replaces command placeholders with local file paths.
3. Runs FFmpeg without a shell.
4. Serves generated outputs from temporary public URLs under `/tmp`.
5. Posts the final result to `webhook_url`.

Output URLs are publicly downloadable for at least 1 hour. Current retention is
2 hours.

### Success Webhook

```json
{
  "data": {
    "status": "COMPLETED",
    "output_files": {
      "out_1": {
        "storage_url": "https://loudness-api.jamrockdev.com/tmp/job-unique-id/VOCALS_abc123.mp3"
      }
    },
    "original_request": {
      "output_files": {
        "out_1": "VOCALS_abc123.mp3"
      }
    },
    "webhook_metadata": {
      "recording_id": "abc123"
    }
  }
}
```

### Failure Webhook

```json
{
  "data": {
    "status": "FAILED",
    "error_message": "Description of what went wrong",
    "error_status": "ffmpeg_error",
    "output_files": {},
    "original_request": {
      "output_files": {
        "out_1": "VOCALS_abc123.mp3"
      }
    },
    "webhook_metadata": {
      "recording_id": "abc123"
    }
  }
}
```

### Required Webhook Fields

The webhook receiver relies on these exact fields:

| Field | Requirement |
| --- | --- |
| `data.status` | Must be `COMPLETED` or `FAILED`. |
| `data.output_files.out_1.storage_url` | Public URL where the output file can be downloaded. |
| `data.original_request.output_files.out_1` | Original output filename string. |
| `data.webhook_metadata` | Passed through unchanged. |

### Filename Prefix Routing

| Prefix | Meaning | Triggered by |
| --- | --- | --- |
| `VOCALS_` | Vocal extraction from video recording | `process-video-audio` |
| `COMBINED_` | Video merged with vocals and backing track | `process-video-merge` |
| `KIOSK_VOCALS_` | Kiosk vocal extraction | `kiosk-submit-recording` |
| `KIOSK_PREPROCESSED_` | Kiosk preprocessed audio | `kiosk-preprocess-audio` |

### Supported Command Examples

Vocal extraction:

```bash
-i {{in_1}} -vn -acodec libmp3lame -q:a 2 {{out_1}}
```

Video merge with backing track:

```bash
-i {{in_1}} -i {{in_2}} -i {{in_3}} -filter_complex "[1:a]volume=0.5[backing];[2:a]volume=2dB[vocals];[backing][vocals]amix=inputs=2:duration=longest[a]" -map 0:v -map "[a]" -c:v copy -c:a aac -ac 2 {{out_1}}
```

Video merge legacy:

```bash
-i {{in_1}} -i {{in_2}} -filter_complex "[0:a][1:a]amerge=inputs=2[a]" -map 0:v -map "[a]" -c:v copy -c:a aac -ac 2 {{out_1}}
```

Kiosk audio preprocessing:

```bash
-i {{in_1}} -af highpass=f=80,afftdn=nf=-25,agate=threshold=0.01:attack=1:release=100,loudnorm=I=-13:TP=-1.5:LR=11 -codec:a libmp3lame -q:a 2 {{out_1}}
```

The loudnorm `I` value may vary per song. The server accepts the legacy
`LR=11` spelling shown above and normalizes it to FFmpeg's `LRA=11` option
before execution.

### cURL Example

```bash
curl -X POST https://loudness-api.jamrockdev.com/run-ffmpeg-command \
  -H "Content-Type: application/json" \
  -d '{
    "input_files": {
      "in_1": "https://storage.rendi.dev/sample/big_buck_bunny_720p_5sec_intro.mp4"
    },
    "output_files": {
      "out_1": "VOCALS_test.mp3"
    },
    "ffmpeg_command": "-i {{in_1}} -vn -acodec libmp3lame -q:a 2 {{out_1}}",
    "webhook_url": "https://fvaaxuhjofhbngkalqcv.supabase.co/functions/v1/receive-vocals",
    "webhook_metadata": {
      "recording_id": "test"
    }
  }'
```

## Shared Error Response

Synchronous validation errors return HTTP `4xx` or `5xx`:

```json
{
  "success": false,
  "error": "Description of what went wrong"
}
```

Common status codes:

- `400`: Required field missing or invalid.
- `413`: Request body is too large.
- `415`: `Content-Type` is not `application/json`.
- `422`: FFmpeg could not decode or process media for the loudness endpoint.
- `502`: The input URL was unreachable for the loudness endpoint.
- `504`: The loudness endpoint exceeded its synchronous timeout.
- `500`: Internal server error.
