Skip to content

Commit be56347

Browse files
Merge pull request #23 from LelabsTeam/feat/site-2260
feat/site-2260: sync with upstream and fix canonical without query params
2 parents 6f6c580 + 9f70b03 commit be56347

File tree

310 files changed

+39801
-1010
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

310 files changed

+39801
-1010
lines changed

1001fx/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# 1001fx MCP
2+
3+
This app integrates with the 1001fx API to provide media processing capabilities for your applications.
4+
5+
## Features
6+
7+
### Audio2Video
8+
9+
Converts an MP3 audio file into an MP4 video file with images displayed for specified durations. The result is a URL to the MP4 file that is available for 48 hours.
10+
11+
#### Parameters
12+
13+
- `url` (string, required): The URL of the audio file (MP3).
14+
- `images` (array, optional): Array of images to show in your video. Each object contains:
15+
- `imageUrl` (string, required): The URL of an image to show in your video.
16+
- `duration` (number, required): The number of seconds this image should be shown.
17+
- `thumbnailImageUrl` (string, optional): The URL of the thumbnail image.
18+
- `videoResolution` (object, optional): The resolution of the output video.
19+
- `width` (number): Width in pixels (default: 1280).
20+
- `height` (number): Height in pixels (default: 720).
21+
- `presignedUrl` (string, optional): When provided, the video URL will be uploaded to this URL rather than returned directly.
22+
23+
#### Example
24+
25+
```typescript
26+
// Basic usage with just audio URL and thumbnail
27+
const result1 = await client.action("1001fx/actions/audio2video", {
28+
url: "https://api.1001fx.com/testdata/jingle.mp3",
29+
thumbnailImageUrl: "https://api.1001fx.com/testdata/image.jpg"
30+
});
31+
32+
// Advanced usage with images and video resolution
33+
const result2 = await client.action("1001fx/actions/audio2video", {
34+
url: "https://api.1001fx.com/testdata/jingle.mp3",
35+
images: [
36+
{
37+
imageUrl: "https://example.com/image1.jpg",
38+
duration: 2
39+
},
40+
{
41+
imageUrl: "https://example.com/image2.jpg",
42+
duration: 3
43+
}
44+
],
45+
thumbnailImageUrl: "https://example.com/thumbnail.jpg",
46+
videoResolution: {
47+
width: 1280,
48+
height: 720
49+
}
50+
});
51+
52+
// Result: { url: "https://cdn.1001fx.com/output/video123.mp4", statusCode: 200 }
53+
```
54+
55+
## Installation
56+
57+
To use this app, you need to provide your 1001fx API key when installing the app.
58+
59+
```typescript
60+
import { createClient } from "deco";
61+
62+
const client = createClient({
63+
apps: {
64+
"1001fx": {
65+
apiKey: "your-api-key"
66+
}
67+
}
68+
});
69+
```

1001fx/actions/audio2video.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { AppContext } from "../mod.ts";
2+
import { isValidUrl, validateImages } from "../utils.ts";
3+
import { ImageConfig, VideoResolution } from "../client.ts";
4+
import { Buffer } from "node:buffer";
5+
6+
export interface Props {
7+
/**
8+
* @description The URL of the audio file (MP3).
9+
* @example https://api.1001fx.com/testdata/jingle.mp3
10+
*/
11+
url: string;
12+
13+
/**
14+
* @description Array of images to show in your video. The length of your video will be the sum of your image durations.
15+
* It is recommended that all your images have the same aspect ratio.
16+
* @minItems 1
17+
* @maxItems 10
18+
*/
19+
images: ImageConfig[];
20+
21+
/**
22+
* @description The URL of the thumbnail image (optional).
23+
* @example https://api.1001fx.com/testdata/image.jpg
24+
*/
25+
thumbnailImageUrl?: string;
26+
27+
/**
28+
* @description The resolution of the output video (optional).
29+
* @default { "width": 1280, "height": 720 }
30+
*/
31+
videoResolution?: VideoResolution;
32+
33+
/**
34+
* @description The presigned URLs to upload the result to. When provided, the video URL will be
35+
* uploaded to this URL rather than returned directly. This allows for easier integration with storage systems.
36+
*/
37+
presignedUrl: string;
38+
}
39+
40+
interface AudioVideoResponse {
41+
url: string;
42+
statusCode: number;
43+
}
44+
45+
/**
46+
* @title Convert MP3 to MP4 with Images
47+
* @description Converts an MP3 audio file into an MP4 video file with images displayed for specified durations.
48+
* The service adds images to the audio and creates a video file. The result is a URL to the MP4 file that is
49+
* available for 48 hours.
50+
*/
51+
export default async function audio2videoAction(
52+
props: Props,
53+
_req: Request,
54+
ctx: AppContext,
55+
): Promise<AudioVideoResponse> {
56+
const { url, images, thumbnailImageUrl, videoResolution, presignedUrl } =
57+
props;
58+
59+
// Validate inputs
60+
if (!isValidUrl(url)) {
61+
throw new Error("Invalid audio URL provided");
62+
}
63+
64+
if (images && !validateImages(images)) {
65+
throw new Error(
66+
"Invalid images array: must contain 1-10 valid image objects with URLs and durations",
67+
);
68+
}
69+
70+
if (thumbnailImageUrl && !isValidUrl(thumbnailImageUrl)) {
71+
throw new Error("Invalid thumbnail image URL provided");
72+
}
73+
74+
try {
75+
// Prepare request body
76+
const requestBody = {
77+
url,
78+
...(images && { images }),
79+
...(thumbnailImageUrl && { thumbnailImageUrl }),
80+
...(videoResolution && { videoResolution }),
81+
};
82+
83+
// Make API call to 1001fx
84+
const response = await ctx.api["POST /audiovideo/audio2video"](
85+
{},
86+
{ body: requestBody },
87+
);
88+
89+
const data = await response.json();
90+
91+
// If presignedUrl is provided, upload the result
92+
if (presignedUrl && isValidUrl(presignedUrl)) {
93+
await uploadToPresignedUrl(data.result.url, presignedUrl);
94+
return {
95+
url: presignedUrl.split("?")[0].replace("_presigned/", ""), // Return clean URL without query params
96+
statusCode: 200,
97+
};
98+
}
99+
100+
// Return the direct result from 1001fx
101+
return {
102+
url: response.url,
103+
statusCode: response.statusCode,
104+
};
105+
} catch (error) {
106+
console.error("Error in audio2video conversion:", error);
107+
108+
// If we have a presignedUrl, try to write error message to it
109+
if (presignedUrl) {
110+
await writeErrorToPresignedUrl(
111+
presignedUrl,
112+
error instanceof Error ? error.message : "Unknown error occurred",
113+
);
114+
}
115+
116+
throw error;
117+
}
118+
}
119+
120+
/**
121+
* Uploads the video URL result to a presigned URL
122+
*/
123+
async function uploadToPresignedUrl(
124+
videoUrl: string,
125+
presignedUrl: string,
126+
): Promise<void> {
127+
try {
128+
// Create a JSON response with the video URL
129+
const videoResponse = await fetch(videoUrl);
130+
const arrayBuffer = await videoResponse.arrayBuffer();
131+
const buffer = Buffer.from(arrayBuffer);
132+
133+
// Upload to presigned URL
134+
const response = await fetch(presignedUrl, {
135+
method: "PUT",
136+
headers: {
137+
"Content-Type": "application/octet-stream",
138+
},
139+
body: buffer,
140+
});
141+
142+
if (!response.ok) {
143+
throw new Error(
144+
`Failed to upload to presigned URL: ${response.status} ${response.statusText}`,
145+
);
146+
}
147+
} catch (error) {
148+
console.error("Error uploading to presigned URL:", error);
149+
throw error;
150+
}
151+
}
152+
153+
/**
154+
* Writes an error message to the presigned URL
155+
*/
156+
async function writeErrorToPresignedUrl(
157+
presignedUrl: string,
158+
errorMessage: string,
159+
): Promise<void> {
160+
try {
161+
const errorJson = JSON.stringify({
162+
error: errorMessage,
163+
statusCode: 5,
164+
});
165+
166+
const response = await fetch(presignedUrl, {
167+
method: "PUT",
168+
headers: {
169+
"Content-Type": "application/json",
170+
},
171+
body: errorJson,
172+
});
173+
174+
if (!response.ok) {
175+
console.error(
176+
`Failed to write error to presigned URL: ${response.status} ${response.statusText}`,
177+
);
178+
}
179+
} catch (error) {
180+
console.error("Error writing error to presigned URL:", error);
181+
}
182+
}

1001fx/client.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { createHttpClient } from "../utils/http.ts";
2+
import { fetchSafe } from "../utils/fetch.ts";
3+
4+
export interface ImageConfig {
5+
imageUrl: string;
6+
duration: number;
7+
}
8+
9+
export interface VideoResolution {
10+
width: number;
11+
height: number;
12+
}
13+
14+
export interface Audio2VideoRequest {
15+
url: string;
16+
images?: ImageConfig[];
17+
thumbnailImageUrl?: string;
18+
videoResolution?: VideoResolution;
19+
}
20+
21+
export interface Audio2VideoResponse {
22+
url: string;
23+
statusCode: number;
24+
}
25+
26+
export interface FX1001Client {
27+
"POST /audiovideo/audio2video": {
28+
body: Audio2VideoRequest;
29+
response: Audio2VideoResponse;
30+
};
31+
}
32+
33+
export const createFX1001Client = (
34+
apiKey: string,
35+
base = "https://api.1001fx.com",
36+
) => {
37+
return createHttpClient<FX1001Client>({
38+
base,
39+
headers: new Headers({
40+
"Content-Type": "application/json",
41+
"x-api-key": apiKey,
42+
}),
43+
fetcher: fetchSafe,
44+
});
45+
};

1001fx/manifest.gen.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// DO NOT EDIT. This file is generated by deco.
2+
// This file SHOULD be checked into source version control.
3+
// This file is automatically updated during development when running `dev.ts`.
4+
5+
import * as $$$$$$$$$0 from "./actions/audio2video.ts";
6+
7+
const manifest = {
8+
"actions": {
9+
"1001fx/actions/audio2video.ts": $$$$$$$$$0,
10+
},
11+
"name": "1001fx",
12+
"baseUrl": import.meta.url,
13+
};
14+
15+
export type Manifest = typeof manifest;
16+
17+
export default manifest;

1001fx/mod.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { type App, type AppContext as AC } from "@deco/deco";
2+
import manifest, { Manifest } from "./manifest.gen.ts";
3+
import { createFX1001Client } from "./client.ts";
4+
import { Secret } from "../website/loaders/secret.ts";
5+
6+
export interface State {
7+
api: ReturnType<typeof createFX1001Client>;
8+
}
9+
10+
export interface Props {
11+
/**
12+
* @description Your 1001fx API key
13+
*/
14+
apiKey: Secret | string;
15+
}
16+
17+
/**
18+
* @title 1001fx
19+
* @name 1001fx
20+
* @description 1001fx API integration for audio and video processing
21+
* @category Media
22+
* @logo https://1001fx.com/favicon.ico
23+
*/
24+
export default function FX1001App(props: Props): App<Manifest, State> {
25+
const { apiKey } = props;
26+
27+
const api = createFX1001Client(
28+
typeof apiKey === "string" ? apiKey : apiKey.get() || "",
29+
);
30+
31+
return {
32+
state: {
33+
api,
34+
},
35+
manifest,
36+
dependencies: [],
37+
};
38+
}
39+
40+
export type FX1001App = ReturnType<typeof FX1001App>;
41+
export type AppContext = AC<FX1001App>;

0 commit comments

Comments
 (0)