@liqhtworks/sophon-sdk 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +14 -1
- package/dist/apis/DownloadsApi.d.ts +1 -1
- package/dist/apis/DownloadsApi.js +1 -1
- package/dist/apis/HealthApi.d.ts +1 -1
- package/dist/apis/HealthApi.js +1 -1
- package/dist/apis/JobsApi.d.ts +5 -5
- package/dist/apis/JobsApi.js +3 -3
- package/dist/apis/UploadsApi.d.ts +1 -1
- package/dist/apis/UploadsApi.js +1 -1
- package/dist/apis/WebhooksApi.d.ts +1 -1
- package/dist/apis/WebhooksApi.js +1 -1
- package/dist/esm/apis/DownloadsApi.d.ts +1 -1
- package/dist/esm/apis/DownloadsApi.js +1 -1
- package/dist/esm/apis/HealthApi.d.ts +1 -1
- package/dist/esm/apis/HealthApi.js +1 -1
- package/dist/esm/apis/JobsApi.d.ts +5 -5
- package/dist/esm/apis/JobsApi.js +3 -3
- package/dist/esm/apis/UploadsApi.d.ts +1 -1
- package/dist/esm/apis/UploadsApi.js +1 -1
- package/dist/esm/apis/WebhooksApi.d.ts +1 -1
- package/dist/esm/apis/WebhooksApi.js +1 -1
- package/dist/esm/helpers/uploads.js +9 -2
- package/dist/esm/models/CompleteUploadResponse.d.ts +1 -1
- package/dist/esm/models/CompleteUploadResponse.js +1 -1
- package/dist/esm/models/CreateJobOutputOptions.d.ts +1 -1
- package/dist/esm/models/CreateJobOutputOptions.js +1 -1
- package/dist/esm/models/CreateJobRequest.d.ts +1 -1
- package/dist/esm/models/CreateJobRequest.js +1 -1
- package/dist/esm/models/CreateUploadRequest.d.ts +1 -1
- package/dist/esm/models/CreateUploadRequest.js +1 -1
- package/dist/esm/models/CreateUploadResponse.d.ts +1 -1
- package/dist/esm/models/CreateUploadResponse.js +1 -1
- package/dist/esm/models/CreateWebhookRequest.d.ts +1 -1
- package/dist/esm/models/CreateWebhookRequest.js +1 -1
- package/dist/esm/models/ErrorBody.d.ts +1 -1
- package/dist/esm/models/ErrorBody.js +1 -1
- package/dist/esm/models/ErrorEnvelope.d.ts +1 -1
- package/dist/esm/models/ErrorEnvelope.js +1 -1
- package/dist/esm/models/JobOutputInfo.d.ts +1 -1
- package/dist/esm/models/JobOutputInfo.js +1 -1
- package/dist/esm/models/JobProfile.d.ts +53 -9
- package/dist/esm/models/JobProfile.js +53 -9
- package/dist/esm/models/JobProgress.d.ts +1 -1
- package/dist/esm/models/JobProgress.js +1 -1
- package/dist/esm/models/JobResponse.d.ts +6 -3
- package/dist/esm/models/JobResponse.js +1 -1
- package/dist/esm/models/JobSourceInfo.d.ts +1 -1
- package/dist/esm/models/JobSourceInfo.js +1 -1
- package/dist/esm/models/JobSourceType.d.ts +1 -1
- package/dist/esm/models/JobSourceType.js +1 -1
- package/dist/esm/models/JobStatus.d.ts +1 -1
- package/dist/esm/models/JobStatus.js +1 -1
- package/dist/esm/models/ListJobsResponse.d.ts +1 -1
- package/dist/esm/models/ListJobsResponse.js +1 -1
- package/dist/esm/models/OutputContainer.d.ts +1 -1
- package/dist/esm/models/OutputContainer.js +1 -1
- package/dist/esm/models/ReadyResponse.d.ts +1 -1
- package/dist/esm/models/ReadyResponse.js +1 -1
- package/dist/esm/models/UploadJobSource.d.ts +1 -1
- package/dist/esm/models/UploadJobSource.js +1 -1
- package/dist/esm/models/UploadPartResponse.d.ts +1 -1
- package/dist/esm/models/UploadPartResponse.js +1 -1
- package/dist/esm/models/UploadStatusResponse.d.ts +1 -1
- package/dist/esm/models/UploadStatusResponse.js +1 -1
- package/dist/esm/models/WebhookDeliveryPayload.d.ts +1 -1
- package/dist/esm/models/WebhookDeliveryPayload.js +1 -1
- package/dist/esm/models/WebhookListItem.d.ts +1 -1
- package/dist/esm/models/WebhookListItem.js +1 -1
- package/dist/esm/models/WebhookListResponse.d.ts +1 -1
- package/dist/esm/models/WebhookListResponse.js +1 -1
- package/dist/esm/models/WebhookResponse.d.ts +1 -1
- package/dist/esm/models/WebhookResponse.js +1 -1
- package/dist/esm/runtime.d.ts +1 -1
- package/dist/esm/runtime.js +1 -1
- package/dist/helpers/uploads.js +9 -2
- package/dist/models/CompleteUploadResponse.d.ts +1 -1
- package/dist/models/CompleteUploadResponse.js +1 -1
- package/dist/models/CreateJobOutputOptions.d.ts +1 -1
- package/dist/models/CreateJobOutputOptions.js +1 -1
- package/dist/models/CreateJobRequest.d.ts +1 -1
- package/dist/models/CreateJobRequest.js +1 -1
- package/dist/models/CreateUploadRequest.d.ts +1 -1
- package/dist/models/CreateUploadRequest.js +1 -1
- package/dist/models/CreateUploadResponse.d.ts +1 -1
- package/dist/models/CreateUploadResponse.js +1 -1
- package/dist/models/CreateWebhookRequest.d.ts +1 -1
- package/dist/models/CreateWebhookRequest.js +1 -1
- package/dist/models/ErrorBody.d.ts +1 -1
- package/dist/models/ErrorBody.js +1 -1
- package/dist/models/ErrorEnvelope.d.ts +1 -1
- package/dist/models/ErrorEnvelope.js +1 -1
- package/dist/models/JobOutputInfo.d.ts +1 -1
- package/dist/models/JobOutputInfo.js +1 -1
- package/dist/models/JobProfile.d.ts +53 -9
- package/dist/models/JobProfile.js +53 -9
- package/dist/models/JobProgress.d.ts +1 -1
- package/dist/models/JobProgress.js +1 -1
- package/dist/models/JobResponse.d.ts +6 -3
- package/dist/models/JobResponse.js +1 -1
- package/dist/models/JobSourceInfo.d.ts +1 -1
- package/dist/models/JobSourceInfo.js +1 -1
- package/dist/models/JobSourceType.d.ts +1 -1
- package/dist/models/JobSourceType.js +1 -1
- package/dist/models/JobStatus.d.ts +1 -1
- package/dist/models/JobStatus.js +1 -1
- package/dist/models/ListJobsResponse.d.ts +1 -1
- package/dist/models/ListJobsResponse.js +1 -1
- package/dist/models/OutputContainer.d.ts +1 -1
- package/dist/models/OutputContainer.js +1 -1
- package/dist/models/ReadyResponse.d.ts +1 -1
- package/dist/models/ReadyResponse.js +1 -1
- package/dist/models/UploadJobSource.d.ts +1 -1
- package/dist/models/UploadJobSource.js +1 -1
- package/dist/models/UploadPartResponse.d.ts +1 -1
- package/dist/models/UploadPartResponse.js +1 -1
- package/dist/models/UploadStatusResponse.d.ts +1 -1
- package/dist/models/UploadStatusResponse.js +1 -1
- package/dist/models/WebhookDeliveryPayload.d.ts +1 -1
- package/dist/models/WebhookDeliveryPayload.js +1 -1
- package/dist/models/WebhookListItem.d.ts +1 -1
- package/dist/models/WebhookListItem.js +1 -1
- package/dist/models/WebhookListResponse.d.ts +1 -1
- package/dist/models/WebhookListResponse.js +1 -1
- package/dist/models/WebhookResponse.d.ts +1 -1
- package/dist/models/WebhookResponse.js +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/docs/JobProfile.md +1 -1
- package/docs/JobsApi.md +1 -1
- package/package.json +1 -1
- package/src/apis/DownloadsApi.ts +1 -1
- package/src/apis/HealthApi.ts +1 -1
- package/src/apis/JobsApi.ts +5 -5
- package/src/apis/UploadsApi.ts +1 -1
- package/src/apis/WebhooksApi.ts +1 -1
- package/src/helpers/uploads.ts +9 -2
- package/src/models/CompleteUploadResponse.ts +1 -1
- package/src/models/CreateJobOutputOptions.ts +1 -1
- package/src/models/CreateJobRequest.ts +1 -1
- package/src/models/CreateUploadRequest.ts +1 -1
- package/src/models/CreateUploadResponse.ts +1 -1
- package/src/models/CreateWebhookRequest.ts +1 -1
- package/src/models/ErrorBody.ts +1 -1
- package/src/models/ErrorEnvelope.ts +1 -1
- package/src/models/JobOutputInfo.ts +1 -1
- package/src/models/JobProfile.ts +53 -9
- package/src/models/JobProgress.ts +1 -1
- package/src/models/JobResponse.ts +6 -3
- package/src/models/JobSourceInfo.ts +1 -1
- package/src/models/JobSourceType.ts +1 -1
- package/src/models/JobStatus.ts +1 -1
- package/src/models/ListJobsResponse.ts +1 -1
- package/src/models/OutputContainer.ts +1 -1
- package/src/models/ReadyResponse.ts +1 -1
- package/src/models/UploadJobSource.ts +1 -1
- package/src/models/UploadPartResponse.ts +1 -1
- package/src/models/UploadStatusResponse.ts +1 -1
- package/src/models/WebhookDeliveryPayload.ts +1 -1
- package/src/models/WebhookListItem.ts +1 -1
- package/src/models/WebhookListResponse.ts +1 -1
- package/src/models/WebhookResponse.ts +1 -1
- package/src/runtime.ts +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/* eslint-disable */
|
|
4
4
|
/**
|
|
5
5
|
* SOPHON Encoding API
|
|
6
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
6
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
7
7
|
*
|
|
8
8
|
* The version of the OpenAPI document: 1.0.0
|
|
9
9
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SOPHON Encoding API
|
|
3
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
3
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
4
4
|
*
|
|
5
5
|
* The version of the OpenAPI document: 1.0.0
|
|
6
6
|
*
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/* eslint-disable */
|
|
4
4
|
/**
|
|
5
5
|
* SOPHON Encoding API
|
|
6
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
6
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
7
7
|
*
|
|
8
8
|
* The version of the OpenAPI document: 1.0.0
|
|
9
9
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SOPHON Encoding API
|
|
3
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
3
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
4
4
|
*
|
|
5
5
|
* The version of the OpenAPI document: 1.0.0
|
|
6
6
|
*
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/* eslint-disable */
|
|
4
4
|
/**
|
|
5
5
|
* SOPHON Encoding API
|
|
6
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
6
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
7
7
|
*
|
|
8
8
|
* The version of the OpenAPI document: 1.0.0
|
|
9
9
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SOPHON Encoding API
|
|
3
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
3
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
4
4
|
*
|
|
5
5
|
* The version of the OpenAPI document: 1.0.0
|
|
6
6
|
*
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/* eslint-disable */
|
|
4
4
|
/**
|
|
5
5
|
* SOPHON Encoding API
|
|
6
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
6
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
7
7
|
*
|
|
8
8
|
* The version of the OpenAPI document: 1.0.0
|
|
9
9
|
*
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SOPHON Encoding API
|
|
3
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
3
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
4
4
|
*
|
|
5
5
|
* The version of the OpenAPI document: 1.0.0
|
|
6
6
|
*
|
package/dist/runtime.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/* eslint-disable */
|
|
4
4
|
/**
|
|
5
5
|
* SOPHON Encoding API
|
|
6
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
6
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
7
7
|
*
|
|
8
8
|
* The version of the OpenAPI document: 1.0.0
|
|
9
9
|
*
|
package/docs/JobProfile.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# JobProfile
|
|
3
3
|
|
|
4
|
-
Encoding profile ID. Coffee-themed naming: prep time maps to encode speed, bare name
|
|
4
|
+
Encoding profile ID. Coffee-themed naming: prep time maps to encode speed (espresso = fast, cortado = medium, americano = slow). The naming suffixes compose: - bare name → 8-bit HEVC Main (universal decoder compatibility, default) - `-10bit` suffix → HEVC Main10 output. Requires a decoder that supports Main10 (modern phones, modern TVs, Safari, Chrome with hardware decode). Older / low-end devices may not play Main10 output. Pick only when you know the downstream pipeline supports it. - `-hq` suffix → quality-biased 8-bit variant for heavy source formats (ProRes, DNxHD, high-bitrate camera originals, mastering-grade intermediates). Files will be larger than the standard tier; pick when preserving detail matters more than bitrate. Broad device compatibility (8-bit Main). - `-hq-10bit` suffix → combines HQ with Main10 to preserve 10-bit depth end-to-end. Same Main10 compatibility caveat as above; pick for ProRes 422/4444, DNxHD, BRAW, or camera masters where detail AND bit depth matter and you control the downstream pipeline. **For broad audience playback, pick `sophon-auto` or an explicit 8-bit coffee profile.** `sophon-auto` produces 8-bit Main output regardless of source bit depth. If you\'re not sure which to pick, use `sophon-auto` — the API picks per-source settings tuned for consistent output regardless of what you submit, and automatically re-encodes at stricter settings if the first pass doesn\'t hold up. **8-bit (standard, default):** - `sophon-espresso` — fastest, lowest compression - `sophon-cortado` — balanced speed and quality - `sophon-americano` — slowest, highest compression **8-bit HQ** (max quality preservation for heavy formats): - `sophon-espresso-hq` - `sophon-cortado-hq` - `sophon-americano-hq` **10-bit (HEVC Main10):** - `sophon-espresso-10bit` - `sophon-cortado-10bit` - `sophon-americano-10bit` **10-bit HQ** (max quality preservation AND preserves 10-bit depth): - `sophon-espresso-hq-10bit` - `sophon-cortado-hq-10bit` - `sophon-americano-hq-10bit` **Adaptive dispatcher:** - `sophon-auto` — content-adaptive. The API probes each source, picks tuned settings, and re-encodes at stricter settings if the first pass doesn\'t hold up. `profile` on the job response stays `sophon-auto`; `effective_profile_id` records the concrete variant the API actually ran.
|
|
5
5
|
|
|
6
6
|
## Properties
|
|
7
7
|
|
package/docs/JobsApi.md
CHANGED
|
@@ -95,7 +95,7 @@ example().catch(console.error);
|
|
|
95
95
|
|
|
96
96
|
Submit an encoding job
|
|
97
97
|
|
|
98
|
-
Creates a queued encoding job from a completed upload source.
|
|
98
|
+
Creates a queued encoding job from a completed upload source. **Picking `profile`:** - Use `sophon-auto` unless you have a specific reason not to. It picks per-source settings tuned for consistent output and re-encodes at stricter settings if the first pass doesn\'t hold up. - Use an explicit coffee profile (`sophon-espresso` / `-cortado` / `-americano`) when you want deterministic encoder behavior — same settings regardless of source. - Use an `-hq` variant when the source is a heavy format (ProRes, DNxHD, high-bitrate camera originals). Larger output files, maximum detail preservation. - Use an `-hq-10bit` variant when the source is 10-bit and you want to preserve that depth end-to-end (ProRes 422/4444, DNxHD, BRAW, camera masters). See `JobProfile` for the full enum. `output.target_height` requests an aspect-preserving downscale (width derived from source, both dims rounded to even). If absent or larger than source, output uses source dimensions.
|
|
99
99
|
|
|
100
100
|
### Example
|
|
101
101
|
|
package/package.json
CHANGED
package/src/apis/DownloadsApi.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
/**
|
|
4
4
|
* SOPHON Encoding API
|
|
5
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
5
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
6
6
|
*
|
|
7
7
|
* The version of the OpenAPI document: 1.0.0
|
|
8
8
|
*
|
package/src/apis/HealthApi.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
/**
|
|
4
4
|
* SOPHON Encoding API
|
|
5
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
5
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
6
6
|
*
|
|
7
7
|
* The version of the OpenAPI document: 1.0.0
|
|
8
8
|
*
|
package/src/apis/JobsApi.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
/**
|
|
4
4
|
* SOPHON Encoding API
|
|
5
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
5
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
6
6
|
*
|
|
7
7
|
* The version of the OpenAPI document: 1.0.0
|
|
8
8
|
*
|
|
@@ -98,7 +98,7 @@ export interface JobsApiInterface {
|
|
|
98
98
|
createJobRequestOpts(requestParameters: CreateJobOperationRequest): Promise<runtime.RequestOpts>;
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* Creates a queued encoding job from a completed upload source.
|
|
101
|
+
* Creates a queued encoding job from a completed upload source. **Picking `profile`:** - Use `sophon-auto` unless you have a specific reason not to. It picks per-source settings tuned for consistent output and re-encodes at stricter settings if the first pass doesn\'t hold up. - Use an explicit coffee profile (`sophon-espresso` / `-cortado` / `-americano`) when you want deterministic encoder behavior — same settings regardless of source. - Use an `-hq` variant when the source is a heavy format (ProRes, DNxHD, high-bitrate camera originals). Larger output files, maximum detail preservation. - Use an `-hq-10bit` variant when the source is 10-bit and you want to preserve that depth end-to-end (ProRes 422/4444, DNxHD, BRAW, camera masters). See `JobProfile` for the full enum. `output.target_height` requests an aspect-preserving downscale (width derived from source, both dims rounded to even). If absent or larger than source, output uses source dimensions.
|
|
102
102
|
* @summary Submit an encoding job
|
|
103
103
|
* @param {string} idempotencyKey Client-generated UUID or string for exactly-once semantics. Required on all POST endpoints. Replaying the same key with the same request body returns the original response without side effects.
|
|
104
104
|
* @param {CreateJobRequest} createJobRequest
|
|
@@ -109,7 +109,7 @@ export interface JobsApiInterface {
|
|
|
109
109
|
createJobRaw(requestParameters: CreateJobOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<JobResponse>>;
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* Creates a queued encoding job from a completed upload source.
|
|
112
|
+
* Creates a queued encoding job from a completed upload source. **Picking `profile`:** - Use `sophon-auto` unless you have a specific reason not to. It picks per-source settings tuned for consistent output and re-encodes at stricter settings if the first pass doesn\'t hold up. - Use an explicit coffee profile (`sophon-espresso` / `-cortado` / `-americano`) when you want deterministic encoder behavior — same settings regardless of source. - Use an `-hq` variant when the source is a heavy format (ProRes, DNxHD, high-bitrate camera originals). Larger output files, maximum detail preservation. - Use an `-hq-10bit` variant when the source is 10-bit and you want to preserve that depth end-to-end (ProRes 422/4444, DNxHD, BRAW, camera masters). See `JobProfile` for the full enum. `output.target_height` requests an aspect-preserving downscale (width derived from source, both dims rounded to even). If absent or larger than source, output uses source dimensions.
|
|
113
113
|
* Submit an encoding job
|
|
114
114
|
*/
|
|
115
115
|
createJob(requestParameters: CreateJobOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<JobResponse>;
|
|
@@ -301,7 +301,7 @@ export class JobsApi extends runtime.BaseAPI implements JobsApiInterface {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
/**
|
|
304
|
-
* Creates a queued encoding job from a completed upload source.
|
|
304
|
+
* Creates a queued encoding job from a completed upload source. **Picking `profile`:** - Use `sophon-auto` unless you have a specific reason not to. It picks per-source settings tuned for consistent output and re-encodes at stricter settings if the first pass doesn\'t hold up. - Use an explicit coffee profile (`sophon-espresso` / `-cortado` / `-americano`) when you want deterministic encoder behavior — same settings regardless of source. - Use an `-hq` variant when the source is a heavy format (ProRes, DNxHD, high-bitrate camera originals). Larger output files, maximum detail preservation. - Use an `-hq-10bit` variant when the source is 10-bit and you want to preserve that depth end-to-end (ProRes 422/4444, DNxHD, BRAW, camera masters). See `JobProfile` for the full enum. `output.target_height` requests an aspect-preserving downscale (width derived from source, both dims rounded to even). If absent or larger than source, output uses source dimensions.
|
|
305
305
|
* Submit an encoding job
|
|
306
306
|
*/
|
|
307
307
|
async createJobRaw(requestParameters: CreateJobOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<JobResponse>> {
|
|
@@ -312,7 +312,7 @@ export class JobsApi extends runtime.BaseAPI implements JobsApiInterface {
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
/**
|
|
315
|
-
* Creates a queued encoding job from a completed upload source.
|
|
315
|
+
* Creates a queued encoding job from a completed upload source. **Picking `profile`:** - Use `sophon-auto` unless you have a specific reason not to. It picks per-source settings tuned for consistent output and re-encodes at stricter settings if the first pass doesn\'t hold up. - Use an explicit coffee profile (`sophon-espresso` / `-cortado` / `-americano`) when you want deterministic encoder behavior — same settings regardless of source. - Use an `-hq` variant when the source is a heavy format (ProRes, DNxHD, high-bitrate camera originals). Larger output files, maximum detail preservation. - Use an `-hq-10bit` variant when the source is 10-bit and you want to preserve that depth end-to-end (ProRes 422/4444, DNxHD, BRAW, camera masters). See `JobProfile` for the full enum. `output.target_height` requests an aspect-preserving downscale (width derived from source, both dims rounded to even). If absent or larger than source, output uses source dimensions.
|
|
316
316
|
* Submit an encoding job
|
|
317
317
|
*/
|
|
318
318
|
async createJob(requestParameters: CreateJobOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<JobResponse> {
|
package/src/apis/UploadsApi.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
/**
|
|
4
4
|
* SOPHON Encoding API
|
|
5
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
5
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
6
6
|
*
|
|
7
7
|
* The version of the OpenAPI document: 1.0.0
|
|
8
8
|
*
|
package/src/apis/WebhooksApi.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
/**
|
|
4
4
|
* SOPHON Encoding API
|
|
5
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
5
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
6
6
|
*
|
|
7
7
|
* The version of the OpenAPI document: 1.0.0
|
|
8
8
|
*
|
package/src/helpers/uploads.ts
CHANGED
|
@@ -94,13 +94,17 @@ export async function uploadFile(params: UploadFileParams): Promise<UploadFileRe
|
|
|
94
94
|
// we only need the head size to slice, so use ceil.
|
|
95
95
|
chunkSize = Math.ceil(source.size / totalChunks);
|
|
96
96
|
} else {
|
|
97
|
+
// SOPHON scopes idempotency keys per-route. createUpload and
|
|
98
|
+
// completeUpload are different routes, so a single key on both
|
|
99
|
+
// returns 409. Derive distinct per-route keys from the caller's
|
|
100
|
+
// (or auto-generated) seed so retries still work.
|
|
97
101
|
const session = await api.createUpload({
|
|
98
102
|
createUploadRequest: {
|
|
99
103
|
file_name: fileName,
|
|
100
104
|
file_size: source.size,
|
|
101
105
|
mime_type: mimeType,
|
|
102
106
|
},
|
|
103
|
-
idempotencyKey
|
|
107
|
+
idempotencyKey: `${idempotencyKey}/create`,
|
|
104
108
|
});
|
|
105
109
|
uploadId = session.id;
|
|
106
110
|
chunkSize = session.chunk_size;
|
|
@@ -159,7 +163,10 @@ export async function uploadFile(params: UploadFileParams): Promise<UploadFileRe
|
|
|
159
163
|
|
|
160
164
|
await Promise.all(workers);
|
|
161
165
|
|
|
162
|
-
const done = await api.completeUpload({
|
|
166
|
+
const done = await api.completeUpload({
|
|
167
|
+
id: uploadId,
|
|
168
|
+
idempotencyKey: `${idempotencyKey}/complete`,
|
|
169
|
+
});
|
|
163
170
|
|
|
164
171
|
return { uploadId: done.id, sha256: done.sha256, bytes: done.bytes };
|
|
165
172
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
/**
|
|
4
4
|
* SOPHON Encoding API
|
|
5
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
5
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
6
6
|
*
|
|
7
7
|
* The version of the OpenAPI document: 1.0.0
|
|
8
8
|
*
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
/**
|
|
4
4
|
* SOPHON Encoding API
|
|
5
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
5
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
6
6
|
*
|
|
7
7
|
* The version of the OpenAPI document: 1.0.0
|
|
8
8
|
*
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
/**
|
|
4
4
|
* SOPHON Encoding API
|
|
5
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
5
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
6
6
|
*
|
|
7
7
|
* The version of the OpenAPI document: 1.0.0
|
|
8
8
|
*
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
/**
|
|
4
4
|
* SOPHON Encoding API
|
|
5
|
-
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination.
|
|
5
|
+
* REST API for submitting, monitoring, and retrieving SOPHON encoding jobs. Authentication is via Bearer API key or session cookie. All POST endpoints require an Idempotency-Key header. List endpoints use opaque cursor-based pagination. --- ## Integration example A real-world walkthrough of how [Daisy](https://daisy.so) wires SOPHON into two production flows — user-uploaded video compression and automatic post-generation encoding after video rendering. Both converge on the same adapter and state machine; only the source differs. The patterns below are the ones that transfer cleanly to any integration. ### 1. One thin adapter, one method per endpoint Keep the HTTP surface boring. Axios (or your stack\'s equivalent), a per-endpoint idempotency key, and no enum for profile names: ```ts @Injectable() export class SophonService { private client() { return axios.create({ baseURL: process.env.SOPHON_BASE_URL, headers: { Authorization: `Bearer ${process.env.SOPHON_API_KEY}` }, timeout: 60_000, }); } async createUploadSession(req, idempotencyKey) { /_* POST /v1/uploads *_/ } async uploadChunk(uploadId, partNumber, bytes) { /_* PUT /v1/uploads/{id}/parts/{n} *_/ } async completeUpload(uploadId, idempotencyKey) { /_* POST /v1/uploads/{id}/complete *_/ } async createJob(req, idempotencyKey) { /_* POST /v1/jobs *_/ } async getJob(id) { /_* GET /v1/jobs/{id} *_/ } async downloadOutputStream(jobId) { /_* GET /v1/jobs/{id}/output *_/ } } ``` **Suffix idempotency keys per endpoint.** SOPHON scopes dedupe per route but a shared key collides across retries that hit different endpoints. Do this: ```ts const base = `video:${video.id}:v1`; await sophon.createUploadSession(req, `${base}:create-upload`); await sophon.completeUpload(uploadId, `${base}:complete-upload`); await sophon.createJob(req, `${base}:create-job`); ``` **Profile names are strings, not an enum.** We add and rename profiles (`sophon-espresso` → `sophon-auto` → future variants). A TypeScript union will drift; let the server validate. ### 2. Model your pipeline as a state machine Persist a single `sophonState` JSON column per row. `jobId === null` routes to dispatch; anything else polls that job: ```ts interface SophonState { jobId: string | null; // null = not dispatched; string = poll it uploadId?: string; // persist between upload + createJob profile?: string; // sophon-auto | sophon-espresso | ... dispatchRetries: number; // 3 strikes → fallback downloadRetries: number; lastError?: { stage, code, message, at }; } // In your cron (5-second tick is plenty): if (state.jobId === null) { await dispatch(video, state); // upload + createJob } else { await poll(video, state); // getJob + (if completed) downloadAndComplete } ``` Persisting `uploadId` between the upload completion and the `createJob` call matters — a crash in that window otherwise re-uploads the file. ### 3. Stream for large sources; buffer for small User-uploaded sources can be 1 GB+. Stream S3 → SOPHON in chunks equal to `session.chunk_size` from the createUploadSession response: ```ts async uploadStream(stream, fileName, mimeType, fileSize) { const session = await this.createUploadSession({ file_name: fileName, file_size: fileSize, mime_type: mimeType, }); let partIndex = 0, buffer = Buffer.alloc(0); for await (const chunk of stream) { buffer = Buffer.concat([buffer, chunk]); while (buffer.length >= session.chunk_size) { await this.uploadChunk(session.id, partIndex++, buffer.subarray(0, session.chunk_size)); buffer = buffer.subarray(session.chunk_size); } } if (buffer.length > 0) { await this.uploadChunk(session.id, partIndex, buffer); } return this.completeUpload(session.id); } ``` Generated outputs from a model run are typically <30 MB — for those, a buffered upload path is simpler and avoids managing a stream lifetime. ### 4. Always keep a fallback URL Before a row enters your encoding state, make sure the source is already playable from your CDN. Every SOPHON failure then degrades to \"use the original\" — the user\'s video never disappears because SOPHON is slow or down. This is the single most important invariant: ```ts await videoRepository.update({ id: video.id }, { videoUrl: sourceCloudfrontUrl, // fallback URL, stays intact status: VideoStatus.EncodingPending, sophonState: { jobId: null, profile, dispatchRetries: 0, downloadRetries: 0 }, sourceFileSize: sourceBytes, }); ``` On any terminal failure (structured `retryable: false`, retry budget exhausted, 404 on getJob, 23h stuck-row guard), flip status back to `Done` with `videoUrl` unchanged. SOPHON is enhancement, not a delivery dependency. ### 5. Handle the \"no-gain\" success path `sophon-auto` runs a pre-probe and, when it decides the output wouldn\'t be smaller than the source, returns `final_artifact: \"original\"` and `saved_percent: 0`. Skip the output download — the source already lives in your bucket: ```ts if (job.status === \'completed\') { if (job.final_artifact === \'original\') { // Persist outputFileSize = sourceFileSize so your UI shows // \"no reduction\" instead of a missing value. await completeWithFallbackOutput(video, job.output?.bytes ?? null); return; } await downloadAndComplete(video, state, job.output?.bytes ?? null); } ``` ### 6. Finalize by streaming into your own storage `GET /v1/jobs/{id}/output` returns a 302 to a presigned URL with a 24h TTL. Stream that directly into your bucket — no temp file, no buffering: ```ts const { stream } = await sophon.downloadOutputStream(state.jobId); const outputKey = `encoded/${video.userId}/${video.id}.mp4`; await fileService.uploadStream(outputKey, stream, \'video/mp4\'); await videoRepository.update({ id: video.id }, { videoUrl: fileService.cloudfrontUrl(outputKey), outputFileSize: sophonOutputBytes, status: VideoStatus.Done, }); ``` ### 7. Failure taxonomy | Error | Handling | |---|---| | Structured `retryable: false` from SOPHON | Terminal. Fall back to `Done` with source URL. | | Retryable upload / createJob failure | Increment `dispatchRetries`; after 3, fall back. | | Retryable download failure | Increment `downloadRetries`; after 3, fall back. | | `getJob` → HTTP 404 | Terminal. Job expired or never created. Fall back. | | Transient poll network error | Do nothing; next tick retries. Don\'t burn retry budget. | | Row stuck in encode state > 23h | Fall back (safety net against orphans). | ### Minimal config ```bash SOPHON_API_KEY=sk_live_... SOPHON_BASE_URL=https://api.liqhtworks.xyz ```
|
|
6
6
|
*
|
|
7
7
|
* The version of the OpenAPI document: 1.0.0
|
|
8
8
|
*
|