@liqhtworks/sophon-sdk 0.1.0 → 0.1.3
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/open-generated-pr.yml +63 -0
- package/.github/workflows/publish.yml +42 -6
- 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.d.ts +0 -3
- 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.d.ts +0 -3
- 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 +15 -5
- 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
|
@@ -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
|
*
|
|
@@ -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
|
*
|
|
@@ -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
|
*
|