Metadata-Version: 2.4
Name: diffio
Version: 0.2.17
Summary: Python SDK for the Diffio API.
Author: Diffio
License: MIT
Project-URL: Homepage, https://diffio.ai
Project-URL: Repository, https://github.com/Diffio-AI/diffio-python
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.24.0
Requires-Dist: svix<2.0.0,>=1.84.1
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: pytest>=7.4; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Dynamic: license-file

# Diffio Python SDK

The Diffio Python SDK helps you call the Diffio API from Python. This version covers project creation, upload, generation, progress checks, and download URLs.
Requires Python 3.8 or later.

## Install

```bash
pip install diffio
```

For local development:

```bash
cd diffio-python
pip install -e .
```

## Configuration

Set the API key with `DIFFIO_API_KEY`. If you need to set the base URL explicitly, use the production endpoint with `DIFFIO_API_BASE_URL`.

```bash
export DIFFIO_API_KEY="diffio_live_..."
export DIFFIO_API_BASE_URL="https://api.diffio.ai/v1"
```

## Request options

Use request options to override headers, timeouts, retries, or the API key per request.
You can also pass `timeoutInSeconds` as an alias for `timeout`.

Generation creation is retried only when you supply a non-empty `idempotencyKey`,
even when `maxRetries` is configured globally or per request. Without a key, a
timeout or lost response can hide an accepted generation, so the SDK returns the
error after the first attempt. With a key, retries send the same key and payload.
Reuse that key when manually retrying the same operation; use a new key for a new
generation. The audio isolation and restore helpers do not supply a key and do
not automatically retry their generation-creation step.

```py
from diffio import DiffioClient, RequestOptions

client = DiffioClient(apiKey="diffio_live_...")
projects = client.list_projects(
    requestOptions=RequestOptions(
        headers={"X-Debug": "1"},
        timeout=30.0,
        maxRetries=2,
        retryBackoff=0.5,
    )
)
```

## Models

Diffio 4.5 Flash (`diffio-4.5-flash`) and Diffio 4.5 Pro (`diffio-4.5-pro`) are the only
models. `diffio-4.5-flash` is the default and works on every plan; `diffio-4.5-pro` needs a
paid plan. Any other model id raises `ValueError` before a request is sent (the API answers
retired model endpoints with HTTP 410 and code `model_retired`). Existing generations created
with older models keep their `modelKey` and can still be listed, polled, and downloaded.

## Create a project and generation

`create_project` creates the project, uploads the file, and confirms the upload:

1. `POST /v1/create_project` returns the project id and an upload session (`project.upload`):
   `edgeBaseUrl`, `uploadToken`, `objectKey`, `partSizeBytes` (32 MiB), `maxBytes`, `expiresAt`.
2. The file goes to the Diffio upload API at `edgeBaseUrl` in `partSizeBytes` parts
   (`/v1/uploads/start`, `/v1/uploads/parts/{n}`, `/v1/uploads/complete`), authorized with
   `Authorization: Bearer {uploadToken}`. Your API key is never sent to the upload API.
   With `maxRetries` set, a failed part is retried on its own instead of resending the file.
   If a part still fails, the SDK aborts the partial upload and raises `DiffioApiError`.
3. `POST /v1/complete_project_upload` confirms the upload so preprocessing starts. Its answer is
   on `project.uploadCompletion` (`status`, `sizeBytes`). It is idempotent; you can call
   `client.complete_project_upload(apiProjectId=...)` (or `client.projects.complete_upload`) again.

Files larger than the session's `maxBytes` (2 GiB) raise `ValueError` before any bytes are sent.

```py
from diffio import DiffioClient

client = DiffioClient(apiKey="diffio_live_...")
file_path = "sample.wav"
project = client.create_project(
    filePath=file_path,
)

generation = client.create_generation(
    apiProjectId=project.apiProjectId,
    model="diffio-4.5-flash",
    idempotencyKey="restore-job-2026-001",
    requestOptions={"maxRetries": 2},
)

print(generation.generationId)
print(generation.idempotentReplay)
```

Use one stable `idempotencyKey` for every retry of the same logical generation request.
The response's optional `idempotentReplay` value is `True` when the API returns the
result of an earlier request with that key. Use a new key for a different generation.

## Audio isolation helper

```py
from diffio import DiffioClient

client = DiffioClient(apiKey="diffio_live_...")
result = client.audio_isolation.isolate(
    filePath="sample.wav",
    model="diffio-4.5-flash",
)

print(result.generation.generationId)
```

## Restore audio in one call

This helper runs the full flow and returns the downloaded bytes plus a metadata dict.

```py
from diffio import DiffioClient

client = DiffioClient(apiKey="diffio_live_...")
audio_bytes, info = client.restore_audio(
    filePath="sample.wav",
    model="diffio-4.5-flash",
    onProgress=lambda progress: print(progress.status),
)

if info["error"]:
    print(info["error"])
else:
    with open("restored.mp3", "wb") as handle:
        handle.write(audio_bytes)

print(info["apiProjectId"], info["generationId"])
```

## Generation progress

`wait_for_generation` and `generations.wait_for_complete` wait for the overall
`status` to become `complete`. Individual stages can reach 100% while video
restoration or final settlement is still pending; stage progress alone does not
indicate overall completion.

`complete` means restored media is ready. Diffio 4.5 transcribes the recording
before restoration starts, so a completed generation has its transcript; while a
generation runs, transcription can be `pending`, `available`, or `unavailable`.
Read `progress.transcription.status` independently; `progress.transcription` is `None`
for older responses that do not report availability. `progress.stage`,
`progress.stageProgress`, and `progress.queue` (fleet queue position and message)
are set while the API reports them and `None` otherwise. A completed generation
remains successful if transcription is unavailable. Completion webhooks expose
the same optional `event.transcription` object; a later transcript does not emit
another `generation.completed` event.

```py
from diffio import DiffioClient

client = DiffioClient(apiKey="diffio_live_...")
progress = client.generations.get_progress(
    generationId="gen_123",
    apiProjectId="proj_123",
)

print(progress.status)
if progress.transcription is not None:
    print(progress.transcription.status)
```

## Generation download

```py
from diffio import DiffioClient

client = DiffioClient(apiKey="diffio_live_...")
download = client.generations.download(
    generationId="gen_123",
    apiProjectId="proj_123",
    downloadType="mp3",
    downloadFilePath="restored.mp3",
)

print(download.downloadUrl)
```

If you only need the URL, use `client.generations.get_download`. Download URLs point at the
Diffio media host, need no extra headers, and are valid for 6 hours. The response has
`downloadType`, `downloadUrl`, `fileName`, `storagePath`, and `mimeType`.

Set `downloadType="transcript"` to download the transcript JSON artifact when the generation has one.
Pending transcripts return `DiffioApiError` with `statusCode == 409` and
`responseBody["code"] == "TRANSCRIPT_PENDING"`. Unavailable transcripts return
`statusCode == 404` and `responseBody["code"] == "TRANSCRIPT_UNAVAILABLE"`. These
responses include `responseBody["transcription"]["status"]`. Check the error code
to distinguish them from other 409 or 404 errors.

```py
from diffio import DiffioApiError

try:
    transcript = client.generations.download(
        generationId="gen_123",
        apiProjectId="proj_123",
        downloadType="transcript",
        downloadFilePath="word_timestamps.json",
    )
except DiffioApiError as exc:
    body = exc.responseBody if isinstance(exc.responseBody, dict) else {}
    if exc.statusCode == 409 and body.get("code") == "TRANSCRIPT_PENDING":
        print("Transcript is pending; check progress and retry later.")
    elif exc.statusCode == 404 and body.get("code") == "TRANSCRIPT_UNAVAILABLE":
        print("Transcript is unavailable; restored media remains available.")
    else:
        raise
```

`restore_audio(downloadType="transcript")` also makes one download request after
media completion. It does not wait for a pending transcript. With its default
`raiseOnError=False`, it returns `(None, info)` and preserves the API error in
`info["statusCode"]` and `info["responseBody"]`; `info["status"]` can still be
`complete` because media restoration succeeded. With `raiseOnError=True`, it raises
the same `DiffioApiError` and attaches the metadata as `exc.restoreInfo`. Callers
can poll progress and retry the transcript download explicitly. Audio and video
downloads proceed independently of transcription availability.

## Account, keys, usage, and webhook configuration

Agent keys can manage account settings, scoped keys, usage, and webhook endpoints.

```py
settings = client.account.get_settings()
key = client.api_keys.create(
    label="Backend worker",
    scopes=["projects:read", "projects:write", "generations:read", "generations:write", "artifacts:read"],
)
usage = client.usage.summary(apiKeyId=key.keyId)
webhook = client.webhooks.configure(
    mode="live",
    url="https://example.com/webhooks/diffio",
    eventTypes=["generation.completed", "generation.failed"],
    apiKeyId=key.keyId,
)
```

## List projects

```py
from diffio import DiffioClient

client = DiffioClient(apiKey="diffio_live_...")
projects = client.projects.list()

for project in projects.projects:
    print(project.apiProjectId, project.status)
```

## List project generations

```py
from diffio import DiffioClient

client = DiffioClient(apiKey="diffio_live_...")
generations = client.projects.list_generations(apiProjectId="proj_123")

for generation in generations.generations:
    print(generation.generationId, generation.status)
```

## Send a test webhook event

```py
from diffio import DiffioClient

client = DiffioClient(apiKey="diffio_live_...")
event = client.webhooks.send_test_event(
    eventType="generation.completed",
    mode="live",
    samplePayload={"apiProjectId": "proj_123"},
)

print(event.svixMessageId)
```

## Verify webhook signatures

Use the raw request body (bytes) plus the `svix-*` headers and your webhook signing secret.

```py
from fastapi import FastAPI, Request, HTTPException
from diffio import DiffioClient
import os

app = FastAPI()
client = DiffioClient(apiKey=os.environ["DIFFIO_API_KEY"])

@app.post("/webhooks/diffio")
async def diffio_webhook(request: Request):
    payload = await request.body()
    headers = request.headers
    try:
        event = client.webhooks.verify_signature(
            payload=payload,
            headers=headers,
            secret=os.environ["DIFFIO_WEBHOOK_SECRET"],
        )
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid signature")
    print("Webhook received", event.eventType)
    return {"ok": True}
```

## Upgrading to 0.2.0

0.2.0 follows the Diffio 4.5 API. It has breaking changes:

* Only `diffio-4.5-flash` and `diffio-4.5-pro` are accepted, and the default model is
  `diffio-4.5-flash`. `diffio-2`, `diffio-2-flash`, `diffio-3.4`, `diffio-3.5`,
  `diffio-4.0-flash`, and `diffio-4.0-pro` were removed with no aliases.
* `CreateProjectResponse` no longer has `uploadUrl`, `uploadMethod`, or `bucket`. It has
  `upload` (a `ProjectUploadSession`) and `uploadCompletion` (a `CompleteProjectUploadResponse`).
  Uploads use the multipart upload API instead of a single signed PUT.
* `GenerationDownloadResponse` no longer has `bucket`.
* Error messages from the upload API (`{"error": {"code", "message"}}`) become the
  `DiffioApiError` message; the full body stays in `responseBody`.

Earlier SDK releases cannot upload to the current API: `create_project` fails with
`KeyError: 'uploadUrl'`.

## Tutorials

* Audio restoration CLI tutorial: `tutorials/audio-restoration-cli/README.md`

## Runtime compatibility

Use Python 3.8 or later.

## Tests

```bash
cd diffio-python
python -m pip install -r requirements-dev.txt
python -m pytest
```
