Skip to content

Commit a376e60

Browse files
authored
feat: upload-new-faces for face swap when face mappings exist (#110)
1 parent 9033e98 commit a376e60

File tree

4 files changed

+128
-5
lines changed

4 files changed

+128
-5
lines changed

codemod/inject-generate.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ async function main() {
229229
"**/files/**",
230230
"**/image-projects/**",
231231
"**/video-projects/**",
232+
// skip face-swap-photo and face-swap for now, they have special handling for faceMappings
233+
"**/face-swap-photo/**",
234+
"**/face-swap/**",
232235
],
233236
});
234237

src/helpers/generate-type.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,14 @@ export type GenerateRequestType<
4646
CreateRequest,
4747
AssetOverrides extends CreateRequest extends { assets: Record<string, any> }
4848
? FilePathProps<CreateRequest["assets"]> & {
49-
// 🚫 forbid extra keys:
49+
// 🚫 forbid extra keys (except faceMappings):
5050
[K in Exclude<
5151
keyof AssetOverrides,
52-
FilePathKeys<CreateRequest["assets"]>
52+
FilePathKeys<CreateRequest["assets"]> | "faceMappings"
5353
>]: {
5454
ERROR: `AssetOverrides can only contain ${FilePathKeys<
5555
CreateRequest["assets"]
56-
>}`;
56+
>} or faceMappings`;
5757
};
5858
}
5959
: {},

src/resources/v1/face-swap-photo/resource-client.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,25 @@ import { ImageProjectsClient } from "magic-hour/resources/v1/image-projects";
2121
type GenerateRequest = GenerateRequestType<
2222
requests.CreateRequest,
2323
{
24+
/**
25+
* Array of face mappings for individual face swaps. Each newFace will be uploaded if it's a local file path.
26+
*/
27+
faceMappings?:
28+
| {
29+
/**
30+
* The face image that will be used to replace the face in the original_face. This value is either
31+
* - a direct URL to the image file
32+
* - a path to a local file
33+
*
34+
* Note: if the path begins with `api-assets`, it will be assumed to already be uploaded to Magic Hour's storage, and will not be uploaded again.
35+
*/
36+
newFace: string;
37+
/**
38+
* The face detected from the target image. This should correspond to the response from the face detection API.
39+
*/
40+
originalFace: string;
41+
}[]
42+
| undefined;
2443
/**
2544
* This is the image from which the face is extracted. This value is either
2645
* - a direct URL to the image file
@@ -89,8 +108,10 @@ export class FaceSwapPhotoClient extends CoreResourceClient {
89108
} = opts;
90109

91110
const fileClient = new FilesClient(this._client, this._opts);
92-
const { sourceFilePath, targetFilePath, ...restAssets } = request.assets;
111+
const { sourceFilePath, targetFilePath, faceMappings, ...restAssets } =
112+
request.assets;
93113

114+
// Upload main files
94115
if (sourceFilePath) {
95116
getLogger().debug(
96117
`Uploading file ${sourceFilePath} to Magic Hour's storage`,
@@ -116,6 +137,44 @@ export class FaceSwapPhotoClient extends CoreResourceClient {
116137
`Uploaded file ${targetFilePath} to Magic Hour's storage as ${uploadedTargetFilePath}`,
117138
);
118139

140+
// Upload faceMappings newFace files if they exist
141+
let updatedFaceMappings = faceMappings;
142+
if (faceMappings && faceMappings.length > 0) {
143+
getLogger().debug(
144+
`Uploading ${faceMappings.length} newFace files for face mappings`,
145+
);
146+
147+
const uploadPromises = faceMappings.map(async (mapping, index) => {
148+
const { newFace, originalFace } = mapping;
149+
150+
// Check if newFace needs to be uploaded (not already uploaded and not a URL)
151+
if (
152+
newFace &&
153+
!newFace.startsWith("api-assets/") &&
154+
!newFace.startsWith("http")
155+
) {
156+
getLogger().debug(
157+
`Uploading newFace file ${newFace} for face mapping ${index}`,
158+
);
159+
const uploadedNewFace = await fileClient.uploadFile(newFace);
160+
getLogger().info(
161+
`Uploaded newFace file ${newFace} as ${uploadedNewFace} for face mapping ${index}`,
162+
);
163+
return {
164+
newFace: uploadedNewFace,
165+
originalFace,
166+
};
167+
} else {
168+
return {
169+
newFace,
170+
originalFace,
171+
};
172+
}
173+
});
174+
175+
updatedFaceMappings = await Promise.all(uploadPromises);
176+
}
177+
119178
const createResponse = await this.create(
120179
{
121180
...request,
@@ -125,6 +184,7 @@ export class FaceSwapPhotoClient extends CoreResourceClient {
125184
? uploadedSourceFilePath
126185
: sourceFilePath,
127186
targetFilePath: uploadedTargetFilePath,
187+
...(updatedFaceMappings && { faceMappings: updatedFaceMappings }),
128188
},
129189
},
130190
createOpts,

src/resources/v1/face-swap/resource-client.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,25 @@ import { VideoProjectsClient } from "magic-hour/resources/v1/video-projects";
2121
type GenerateRequest = GenerateRequestType<
2222
requests.CreateRequest,
2323
{
24+
/**
25+
* Array of face mappings for individual face swaps. Each newFace will be uploaded if it's a local file path.
26+
*/
27+
faceMappings?:
28+
| {
29+
/**
30+
* The face image that will be used to replace the face in the original_face. This value is either
31+
* - a direct URL to the image file
32+
* - a path to a local file
33+
*
34+
* Note: if the path begins with `api-assets`, it will be assumed to already be uploaded to Magic Hour's storage, and will not be uploaded again.
35+
*/
36+
newFace: string;
37+
/**
38+
* The face detected from the target image. This should correspond to the response from the face detection API.
39+
*/
40+
originalFace: string;
41+
}[]
42+
| undefined;
2443
/**
2544
* The path of the input image with the face to be swapped. This value is either
2645
* - a direct URL to the image file
@@ -93,8 +112,10 @@ export class FaceSwapClient extends CoreResourceClient {
93112
} = opts;
94113

95114
const fileClient = new FilesClient(this._client, this._opts);
96-
const { imageFilePath, videoFilePath, ...restAssets } = request.assets;
115+
const { imageFilePath, videoFilePath, faceMappings, ...restAssets } =
116+
request.assets;
97117

118+
// Upload main files
98119
if (imageFilePath) {
99120
getLogger().debug(
100121
`Uploading file ${imageFilePath} to Magic Hour's storage`,
@@ -126,13 +147,52 @@ export class FaceSwapClient extends CoreResourceClient {
126147
);
127148
}
128149

150+
// Upload faceMappings newFace files if they exist
151+
let updatedFaceMappings = faceMappings;
152+
if (faceMappings && faceMappings.length > 0) {
153+
getLogger().debug(
154+
`Uploading ${faceMappings.length} newFace files for face mappings`,
155+
);
156+
157+
const uploadPromises = faceMappings.map(async (mapping, index) => {
158+
const { newFace, originalFace } = mapping;
159+
160+
// Check if newFace needs to be uploaded (not already uploaded and not a URL)
161+
if (
162+
newFace &&
163+
!newFace.startsWith("api-assets/") &&
164+
!newFace.startsWith("http")
165+
) {
166+
getLogger().debug(
167+
`Uploading newFace file ${newFace} for face mapping ${index}`,
168+
);
169+
const uploadedNewFace = await fileClient.uploadFile(newFace);
170+
getLogger().info(
171+
`Uploaded newFace file ${newFace} as ${uploadedNewFace} for face mapping ${index}`,
172+
);
173+
return {
174+
newFace: uploadedNewFace,
175+
originalFace,
176+
};
177+
} else {
178+
return {
179+
newFace,
180+
originalFace,
181+
};
182+
}
183+
});
184+
185+
updatedFaceMappings = await Promise.all(uploadPromises);
186+
}
187+
129188
const createResponse = await this.create(
130189
{
131190
...request,
132191
assets: {
133192
...restAssets,
134193
imageFilePath: imageFilePath ? uploadedImageFilePath : imageFilePath,
135194
videoFilePath: videoFilePath ? uploadedVideoFilePath : videoFilePath,
195+
...(updatedFaceMappings && { faceMappings: updatedFaceMappings }),
136196
},
137197
},
138198
createOpts,

0 commit comments

Comments
 (0)