react-native-facefusion 0.1.0

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.
Files changed (122) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +259 -0
  3. package/android/build.gradle +129 -0
  4. package/android/src/main/AndroidManifest.xml +66 -0
  5. package/android/src/main/cpp/CMakeLists.txt +73 -0
  6. package/android/src/main/java/com/facefusion/BitmapDecode.kt +112 -0
  7. package/android/src/main/java/com/facefusion/ContentGate.kt +86 -0
  8. package/android/src/main/java/com/facefusion/DeviceProbe.kt +158 -0
  9. package/android/src/main/java/com/facefusion/FaceCrop.kt +57 -0
  10. package/android/src/main/java/com/facefusion/FacefusionModule.kt +396 -0
  11. package/android/src/main/java/com/facefusion/FacefusionPackage.kt +37 -0
  12. package/android/src/main/java/com/facefusion/FacefusionPreviewView.kt +30 -0
  13. package/android/src/main/java/com/facefusion/FacefusionPreviewViewManager.kt +25 -0
  14. package/android/src/main/java/com/facefusion/GallerySave.kt +82 -0
  15. package/android/src/main/java/com/facefusion/ModelDownload.kt +351 -0
  16. package/android/src/main/java/com/facefusion/ModelPaths.kt +120 -0
  17. package/android/src/main/java/com/facefusion/PhotoSwap.kt +125 -0
  18. package/android/src/main/java/com/facefusion/PipeGuard.kt +74 -0
  19. package/android/src/main/java/com/facefusion/PreviewSurfaceHolder.kt +104 -0
  20. package/android/src/main/java/com/facefusion/SourceFaces.kt +125 -0
  21. package/android/src/main/java/com/facefusion/SwapConfig.kt +31 -0
  22. package/android/src/main/java/com/facefusion/TargetFaces.kt +88 -0
  23. package/android/src/main/java/com/facefusion/VideoSwap.kt +501 -0
  24. package/android/src/main/java/com/facefusion/VideoSwapService.kt +85 -0
  25. package/android/src/main/java/com/facefusion/mobile/NativePipe.kt +192 -0
  26. package/lib/module/FacefusionPreview.js +6 -0
  27. package/lib/module/FacefusionPreview.js.map +1 -0
  28. package/lib/module/FacefusionPreview.native.js +15 -0
  29. package/lib/module/FacefusionPreview.native.js.map +1 -0
  30. package/lib/module/NativeFacefusion.js +53 -0
  31. package/lib/module/NativeFacefusion.js.map +1 -0
  32. package/lib/module/detectSourceFaces.js +6 -0
  33. package/lib/module/detectSourceFaces.js.map +1 -0
  34. package/lib/module/detectSourceFaces.native.js +15 -0
  35. package/lib/module/detectSourceFaces.native.js.map +1 -0
  36. package/lib/module/detectTargetFaces.js +6 -0
  37. package/lib/module/detectTargetFaces.js.map +1 -0
  38. package/lib/module/detectTargetFaces.native.js +13 -0
  39. package/lib/module/detectTargetFaces.native.js.map +1 -0
  40. package/lib/module/index.js +11 -0
  41. package/lib/module/index.js.map +1 -0
  42. package/lib/module/models.js +16 -0
  43. package/lib/module/models.js.map +1 -0
  44. package/lib/module/models.native.js +43 -0
  45. package/lib/module/models.native.js.map +1 -0
  46. package/lib/module/package.json +1 -0
  47. package/lib/module/probeDevice.js +6 -0
  48. package/lib/module/probeDevice.js.map +1 -0
  49. package/lib/module/probeDevice.native.js +14 -0
  50. package/lib/module/probeDevice.native.js.map +1 -0
  51. package/lib/module/saveToGallery.js +6 -0
  52. package/lib/module/saveToGallery.js.map +1 -0
  53. package/lib/module/saveToGallery.native.js +15 -0
  54. package/lib/module/saveToGallery.native.js.map +1 -0
  55. package/lib/module/swapPhoto.js +6 -0
  56. package/lib/module/swapPhoto.js.map +1 -0
  57. package/lib/module/swapPhoto.native.js +16 -0
  58. package/lib/module/swapPhoto.native.js.map +1 -0
  59. package/lib/module/swapVideo.js +12 -0
  60. package/lib/module/swapVideo.js.map +1 -0
  61. package/lib/module/swapVideo.native.js +29 -0
  62. package/lib/module/swapVideo.native.js.map +1 -0
  63. package/lib/typescript/package.json +1 -0
  64. package/lib/typescript/src/FacefusionPreview.d.ts +4 -0
  65. package/lib/typescript/src/FacefusionPreview.d.ts.map +1 -0
  66. package/lib/typescript/src/FacefusionPreview.native.d.ts +142 -0
  67. package/lib/typescript/src/FacefusionPreview.native.d.ts.map +1 -0
  68. package/lib/typescript/src/NativeFacefusion.d.ts +291 -0
  69. package/lib/typescript/src/NativeFacefusion.d.ts.map +1 -0
  70. package/lib/typescript/src/detectSourceFaces.d.ts +3 -0
  71. package/lib/typescript/src/detectSourceFaces.d.ts.map +1 -0
  72. package/lib/typescript/src/detectSourceFaces.native.d.ts +11 -0
  73. package/lib/typescript/src/detectSourceFaces.native.d.ts.map +1 -0
  74. package/lib/typescript/src/detectTargetFaces.d.ts +3 -0
  75. package/lib/typescript/src/detectTargetFaces.d.ts.map +1 -0
  76. package/lib/typescript/src/detectTargetFaces.native.d.ts +9 -0
  77. package/lib/typescript/src/detectTargetFaces.native.d.ts.map +1 -0
  78. package/lib/typescript/src/index.d.ts +11 -0
  79. package/lib/typescript/src/index.d.ts.map +1 -0
  80. package/lib/typescript/src/models.d.ts +7 -0
  81. package/lib/typescript/src/models.d.ts.map +1 -0
  82. package/lib/typescript/src/models.native.d.ts +31 -0
  83. package/lib/typescript/src/models.native.d.ts.map +1 -0
  84. package/lib/typescript/src/probeDevice.d.ts +3 -0
  85. package/lib/typescript/src/probeDevice.d.ts.map +1 -0
  86. package/lib/typescript/src/probeDevice.native.d.ts +10 -0
  87. package/lib/typescript/src/probeDevice.native.d.ts.map +1 -0
  88. package/lib/typescript/src/saveToGallery.d.ts +2 -0
  89. package/lib/typescript/src/saveToGallery.d.ts.map +1 -0
  90. package/lib/typescript/src/saveToGallery.native.d.ts +9 -0
  91. package/lib/typescript/src/saveToGallery.native.d.ts.map +1 -0
  92. package/lib/typescript/src/swapPhoto.d.ts +3 -0
  93. package/lib/typescript/src/swapPhoto.d.ts.map +1 -0
  94. package/lib/typescript/src/swapPhoto.native.d.ts +12 -0
  95. package/lib/typescript/src/swapPhoto.native.d.ts.map +1 -0
  96. package/lib/typescript/src/swapVideo.d.ts +6 -0
  97. package/lib/typescript/src/swapVideo.d.ts.map +1 -0
  98. package/lib/typescript/src/swapVideo.native.d.ts +20 -0
  99. package/lib/typescript/src/swapVideo.native.d.ts.map +1 -0
  100. package/package.json +132 -0
  101. package/patches/ffjni-analyse-faces.patch +63 -0
  102. package/scripts/check-tarball.sh +68 -0
  103. package/scripts/fetch-upstream.sh +91 -0
  104. package/src/FacefusionPreview.native.tsx +18 -0
  105. package/src/FacefusionPreview.tsx +9 -0
  106. package/src/NativeFacefusion.ts +324 -0
  107. package/src/detectSourceFaces.native.tsx +17 -0
  108. package/src/detectSourceFaces.tsx +10 -0
  109. package/src/detectTargetFaces.native.tsx +15 -0
  110. package/src/detectTargetFaces.tsx +10 -0
  111. package/src/index.tsx +25 -0
  112. package/src/models.native.tsx +45 -0
  113. package/src/models.tsx +23 -0
  114. package/src/probeDevice.native.tsx +13 -0
  115. package/src/probeDevice.tsx +7 -0
  116. package/src/saveToGallery.native.tsx +16 -0
  117. package/src/saveToGallery.tsx +9 -0
  118. package/src/swapPhoto.native.tsx +20 -0
  119. package/src/swapPhoto.tsx +12 -0
  120. package/src/swapVideo.native.tsx +40 -0
  121. package/src/swapVideo.tsx +31 -0
  122. package/third_party/facefusion-mobile/NOTICE +49 -0
@@ -0,0 +1,324 @@
1
+ import {
2
+ TurboModuleRegistry,
3
+ type CodegenTypes,
4
+ type TurboModule,
5
+ } from 'react-native';
6
+
7
+ /**
8
+ * What the device's Hexagon NPU reports about itself.
9
+ *
10
+ * `ok` is the field that decides whether the rest mean anything. `false` means the probe
11
+ * could not run — no Qualcomm NPU, no QNN runtime, an emulator — and *not* that the chip
12
+ * is too old. `tier` and `tierChain` are still usable in that case: they fall back to
13
+ * `v68`, the build that runs on every Hexagon.
14
+ */
15
+ export type DeviceProbeResult = {
16
+ /** True only when the NPU was actually measured. */
17
+ ok: boolean;
18
+ /** The context-binary tier this chip should download — `v68` … `v81`. */
19
+ tier: string;
20
+ /** Every tier that would load here, best first. The downloader wants this one. */
21
+ tierChain: string[];
22
+ /** Hexagon architecture number: 68, 73, 79, 81. `0` when unmeasured. */
23
+ arch: number;
24
+ /** Tightly-coupled memory in MB. The v73/v79/v81 builds need 8. */
25
+ vtcmMb: number;
26
+ /** Qualcomm's SoC id — 69 is SM8750, the 8 Elite. */
27
+ socModel: number;
28
+ signedPd: boolean;
29
+ dlbc: boolean;
30
+ /** Why the probe failed. Empty when `ok`. */
31
+ error: string;
32
+ };
33
+
34
+ /**
35
+ * What is on disk, and what the pipeline would load right now.
36
+ *
37
+ * `tier` is resolved against the **files present**, not against the chip: a phone whose
38
+ * best architecture has no published models runs the best tier that does. So this can
39
+ * legitimately differ from `DeviceProbeResult.tier`, and this is the one that matters for
40
+ * "can it run".
41
+ */
42
+ export type ModelStatus = {
43
+ /** The tier that will actually be loaded — best tier whose files are on disk. */
44
+ tier: string;
45
+ /** Every tier this chip could load, best first. */
46
+ tierChain: string[];
47
+ /** Absolute path of the models directory. */
48
+ dir: string;
49
+ /** True when every model the pipeline requires is present and verified. */
50
+ ready: boolean;
51
+ /** Base names of the required models still absent, e.g. `['hyperswap', 'nsfw']`. */
52
+ missing: string[];
53
+ /** Whether the optional face enhancer (`gpen`) is present. */
54
+ hasEnhancer: boolean;
55
+ /** True when the current connection is metered — worth a warning before ~317 MB. */
56
+ metered: boolean;
57
+ };
58
+
59
+ /**
60
+ * Tunable knobs for a swap. Every field is optional and defaults to upstream's own default
61
+ * for the `hyperswap` swapper — see `ffpipe::Config` in the vendored C++.
62
+ */
63
+ export type SwapOptions = {
64
+ /** Blends the two identity embeddings before the generator sees them. 0.5 = source,
65
+ * unmodified; above it strengthens the source identity, below it blends the target's
66
+ * identity back in. Default `0.5`. */
67
+ swapperWeight?: number;
68
+ /** Softness of the paste-back mask edge, `0..1`. Default `0.3`. */
69
+ maskBlur?: number;
70
+ /** `[top, right, bottom, left]`, each `0..100` percent. Default `[0, 0, 0, 0]`. */
71
+ maskPadding?: number[];
72
+ /** Minimum detector confidence to count as a face, `0..1`. Default `0.5`. */
73
+ detectorScore?: number;
74
+ /** Minimum landmarker confidence, `0..1`. Default `0.5`. */
75
+ landmarkerScore?: number;
76
+ /** Per-axis upscale of the swap crop: 1 = 256px, 2 = 512px, 3 = 768px, 4 = 1024px. Costs
77
+ * `pixelBoost²` swapper invocations. Default `1`. */
78
+ pixelBoost?: number;
79
+ /** Swap only the largest face instead of every face found. Default `false`. */
80
+ largestFaceOnly?: boolean;
81
+ /** Run the face enhancer after swapping. Silently has no effect if the enhancer model is
82
+ * not on disk — check {@link ModelStatus.hasEnhancer} before offering this. Default
83
+ * `false`. */
84
+ faceEnhance?: boolean;
85
+ /** How much of the enhancer to blend in, `0..1`. `0` is the swapper's output untouched.
86
+ * Default `0.8`. */
87
+ faceEnhancerBlend?: number;
88
+ /**
89
+ * Which face in the source photo to use as the identity, as `[left, top, right,
90
+ * bottom]` from {@link detectSourceFaces} — in the source image's own pixel
91
+ * coordinates, not normalised. Omit for the default: the largest face in the source,
92
+ * same as every swap before this option existed.
93
+ */
94
+ sourceFaceBox?: number[];
95
+ /**
96
+ * Which face in the target to swap, as `[left, top, right, bottom]` from
97
+ * {@link detectTargetFaces} — in the target's own pixel coordinates (for a video, the
98
+ * clip's upright orientation). Omit to swap every face found (subject to
99
+ * {@link SwapOptions.largestFaceOnly}), same as every swap before this option existed.
100
+ * For a video, the box is picked once and held fixed for the whole clip — it is not
101
+ * re-detected frame to frame, so a subject who moves far out of it stops being swapped.
102
+ */
103
+ targetFaceBox?: number[];
104
+ /**
105
+ * `swapVideo` only. Caps how many of the source's frames actually get swapped and
106
+ * encoded — the rest are decoded and dropped, not held back or slowed down, so the
107
+ * output plays at the same real-world duration, just choppier. Omit, or set `>=` the
108
+ * source's own frame rate, to process every frame (default). Lowering this is a direct
109
+ * wall-clock speed lever: half the frames is roughly half the NPU + encode work.
110
+ */
111
+ targetFps?: number;
112
+ };
113
+
114
+ /**
115
+ * Tunable knobs for a detect-only pass — the same two thresholds {@link SwapOptions}
116
+ * carries, and they mean exactly the same thing.
117
+ *
118
+ * **Pass the same values here that you pass to the swap.** Two reasons. The obvious one is
119
+ * that a face picker which finds a face the following swap then rejects is a picker that
120
+ * lies. The non-obvious one is cost: these thresholds reach the native pipeline through
121
+ * `initEx`, so changing one re-opens every model graph. Detecting at the defaults and then
122
+ * swapping at your own values pays that reload twice per swap.
123
+ *
124
+ * Lowering {@link detectorScore} is the fix for "it finds the face on one phone and not on
125
+ * another with the same photo" — see the note on that field.
126
+ */
127
+ export type DetectOptions = {
128
+ /**
129
+ * Minimum detector confidence to count as a face, `0..1`. Default `0.5`, which is
130
+ * upstream FaceFusion's own default.
131
+ *
132
+ * **This is the knob that differs by chip.** Every tier ships the same 4.0 MB yoloface
133
+ * graph, but as a *separately compiled* QNN context binary per Hexagon architecture, so
134
+ * the same photo scores differently on a v68 chip than on a v79 one. The cutoff is hard
135
+ * (`score <= detectorScore` is dropped outright), so a face scoring 0.46 on an older
136
+ * chip and 0.58 on a newer one is found on exactly one of them. Offer this as a control
137
+ * rather than treating "no faces" as final; ~0.3 recovers most of the gap.
138
+ */
139
+ detectorScore?: number;
140
+ /** Minimum landmarker confidence, `0..1`. Default `0.5`. */
141
+ landmarkerScore?: number;
142
+ };
143
+
144
+ /** One face found in a source photo, in the image's own pixel coordinates. */
145
+ export type DetectedFace = {
146
+ left: number;
147
+ top: number;
148
+ right: number;
149
+ bottom: number;
150
+ /** Detector confidence, `0..1`. */
151
+ score: number;
152
+ /**
153
+ * Width of the image **as actually analysed** — the coordinate space `left`/`right`
154
+ * are in, which is *not* the size of the file on disk.
155
+ *
156
+ * Large photos are subsampled during decode so they fit in memory (a 50 MP photo
157
+ * needs ~950 MB uncapped), and a video target is analysed at its own frame size with
158
+ * no cap at all. So the scale factor differs by input type and cannot be worked out
159
+ * from the file — use these two fields.
160
+ *
161
+ * You do not need them to pick a face: pass a box straight back as
162
+ * `SwapOptions.targetFaceBox`/`sourceFaceBox` and it lines up, because the swap crops
163
+ * in this same space. You *do* need them to draw the box over the original image or
164
+ * crop a thumbnail from it:
165
+ *
166
+ * ```ts
167
+ * const scaleX = displayedWidth / face.imageWidth;
168
+ * const scaleY = displayedHeight / face.imageHeight;
169
+ * const rect = {
170
+ * left: face.left * scaleX,
171
+ * top: face.top * scaleY,
172
+ * width: (face.right - face.left) * scaleX,
173
+ * height: (face.bottom - face.top) * scaleY,
174
+ * };
175
+ * ```
176
+ */
177
+ imageWidth: number;
178
+ /** Height of the image as actually analysed — see {@link DetectedFace.imageWidth}. */
179
+ imageHeight: number;
180
+ };
181
+
182
+ /** The result of one still-photo swap. */
183
+ export type SwapPhotoResult = {
184
+ /** Same as the `outputPath` passed in — returned for convenience. */
185
+ outputPath: string;
186
+ /** Faces found in the target. `0` means the swap ran but found nothing to swap, and
187
+ * `outputPath` is then an untouched copy of the target. */
188
+ faceCount: number;
189
+ /** The tier that ran — see {@link ModelStatus.tier}. */
190
+ tier: string;
191
+ };
192
+
193
+ /** The result of one video swap. */
194
+ export type SwapVideoResult = {
195
+ /** Same as the `outputPath` passed in — returned for convenience. */
196
+ outputPath: string;
197
+ /** Frames decoded, swapped and re-encoded — the real count. */
198
+ frameCount: number;
199
+ /** Of `frameCount`, how many had at least one face swapped. */
200
+ faceFrameCount: number;
201
+ /** The tier that ran — see {@link ModelStatus.tier}. */
202
+ tier: string;
203
+ /** Measured wall-clock frames/second across the decode-swap-encode loop. Not upstream's
204
+ * per-graph figure — see `docs/MEMORY.md` rule 11. */
205
+ fps: number;
206
+ /** Whether the source clip had an audio track — if so, it was copied to `outputPath`
207
+ * unmodified. */
208
+ hasAudio: boolean;
209
+ };
210
+
211
+ /** A tick of progress through a video swap. */
212
+ export type VideoSwapProgress = {
213
+ /** Frames decoded, swapped and encoded so far. */
214
+ frameIndex: number;
215
+ /** Estimated from the container's duration and frame rate. `0` when it could not be
216
+ * estimated — a variable-frame-rate source has no true count until the last frame. */
217
+ estimatedFrameCount: number;
218
+ /** Wall-clock frames/second so far. */
219
+ fps: number;
220
+ };
221
+
222
+ /** A download in flight. Byte counts are across the whole set, not the current file. */
223
+ export type ModelDownloadProgress = {
224
+ /** The tier being fetched. May differ from the chip's best tier — see [ModelStatus]. */
225
+ tier: string;
226
+ /** 1-based index of the file being fetched, of `fileCount`. */
227
+ fileIndex: number;
228
+ /** How many files this run has to fetch. Already-present files are not counted. */
229
+ fileCount: number;
230
+ /** File name being fetched, e.g. `hyperswap_v79.bin`. Empty on the final tick. */
231
+ name: string;
232
+ /** Bytes transferred so far across the run, including a resumed `.part`. */
233
+ doneBytes: number;
234
+ /** Bytes this run has to transfer in total. */
235
+ totalBytes: number;
236
+ };
237
+
238
+ export interface Spec extends TurboModule {
239
+ probeDevice(): Promise<DeviceProbeResult>;
240
+ getModelStatus(): Promise<ModelStatus>;
241
+ downloadModels(): Promise<ModelStatus>;
242
+ cancelModelDownload(): void;
243
+ readonly onModelDownloadProgress: CodegenTypes.EventEmitter<ModelDownloadProgress>;
244
+ /**
245
+ * Swaps the face from `sourcePath` into every face found in `targetPath`, writing the
246
+ * result to `outputPath`. Paths in, path out — no pixels cross the bridge.
247
+ *
248
+ * Rejects with `E_BUSY` if a swap or video job is already running, `E_MODELS` if the
249
+ * required models are not on disk yet, `E_CONTENT` if the target was refused by the
250
+ * content gate, and `E_SWAP` otherwise.
251
+ */
252
+ swapPhoto(
253
+ sourcePath: string,
254
+ targetPath: string,
255
+ outputPath: string,
256
+ options?: SwapOptions
257
+ ): Promise<SwapPhotoResult>;
258
+ /**
259
+ * Swaps the face from `sourcePath` into every frame of the video at `targetPath`,
260
+ * writing the result to `outputPath`. Runs behind a foreground service (Android requires
261
+ * one for a job this long) and reports progress via {@link onVideoSwapProgress}.
262
+ *
263
+ * Rejects with `E_BUSY` if a swap or another video job is already running, `E_MODELS` if
264
+ * the required models are not on disk yet, `E_CANCELLED` if {@link cancelVideoSwap} was
265
+ * called, `E_CONTENT` if the target was refused by the content gate, and `E_SWAP`
266
+ * otherwise.
267
+ */
268
+ swapVideo(
269
+ sourcePath: string,
270
+ targetPath: string,
271
+ outputPath: string,
272
+ options?: SwapOptions
273
+ ): Promise<SwapVideoResult>;
274
+ /** Asks the video swap in flight to stop. Fire-and-forget — the answer arrives as the
275
+ * `E_CANCELLED` rejection of the {@link swapVideo} promise, not from here. */
276
+ cancelVideoSwap(): void;
277
+ readonly onVideoSwapProgress: CodegenTypes.EventEmitter<VideoSwapProgress>;
278
+ /**
279
+ * Every face detected in the photo at `sourcePath`, for a UI to let the user pick one
280
+ * before swapping — pass the chosen face's box back as {@link SwapOptions.sourceFaceBox}.
281
+ * Does not swap or modify anything.
282
+ *
283
+ * Rejects with `E_BUSY` if a swap or video job is already running, `E_MODELS` if the
284
+ * required models are not on disk yet, and `E_DETECT` otherwise.
285
+ */
286
+ detectSourceFaces(
287
+ sourcePath: string,
288
+ options?: DetectOptions
289
+ ): Promise<DetectedFace[]>;
290
+ /**
291
+ * Every face detected in the target at `targetPath` — a photo, or a video (its first
292
+ * frame, already upright) — for a UI to let the user pick one before swapping. Pass the
293
+ * chosen face's box back as {@link SwapOptions.targetFaceBox}. Does not swap or modify
294
+ * anything.
295
+ *
296
+ * Rejects with `E_BUSY` if a swap or video job is already running, `E_MODELS` if the
297
+ * required models are not on disk yet, and `E_DETECT` otherwise.
298
+ */
299
+ detectTargetFaces(
300
+ targetPath: string,
301
+ options?: DetectOptions
302
+ ): Promise<DetectedFace[]>;
303
+ /**
304
+ * Copies the file at `path` — a `swapPhoto`/`swapVideo` output, typically — into the
305
+ * system's Photos/Gallery app, under a `Facefusion` album. Returns the resulting
306
+ * `content://` URI as a string.
307
+ *
308
+ * `path` and the app that owns it stay exactly as they were; this is a copy, not a move.
309
+ * `mimeType` must be `image/*` or `video/*` — pass the real one for the file (`image/jpeg`
310
+ * for `swapPhoto`'s default output, `video/mp4` for `swapVideo`'s), since there is no safe
311
+ * way to guess it from an arbitrary path. `displayName` defaults to `path`'s own filename.
312
+ *
313
+ * No storage permission is needed for this on Android — inserting new media an app itself
314
+ * created has never required one under scoped storage (API 29+). Rejects with `E_MIME` for
315
+ * an unsupported `mimeType` and `E_SAVE` for anything else, naming what failed.
316
+ */
317
+ saveToGallery(
318
+ path: string,
319
+ mimeType: string,
320
+ displayName?: string
321
+ ): Promise<string>;
322
+ }
323
+
324
+ export default TurboModuleRegistry.getEnforcing<Spec>('Facefusion');
@@ -0,0 +1,17 @@
1
+ import Facefusion from './NativeFacefusion';
2
+ import type { DetectedFace, DetectOptions } from './NativeFacefusion';
3
+
4
+ /**
5
+ * Every face detected in the photo at `sourcePath`, for a UI to let the user pick one
6
+ * before swapping — pass the chosen face's box back as `sourceFaceBox` in `SwapOptions`.
7
+ * Does not swap or modify anything.
8
+ *
9
+ * Rejects with `E_BUSY` if a swap or video job is already running, `E_MODELS` if the
10
+ * required models are not on disk yet, and `E_DETECT` otherwise.
11
+ */
12
+ export function detectSourceFaces(
13
+ sourcePath: string,
14
+ options?: DetectOptions
15
+ ): Promise<DetectedFace[]> {
16
+ return Facefusion.detectSourceFaces(sourcePath, options);
17
+ }
@@ -0,0 +1,10 @@
1
+ import type { DetectedFace, DetectOptions } from './NativeFacefusion';
2
+
3
+ export function detectSourceFaces(
4
+ _sourcePath: string,
5
+ _options?: DetectOptions
6
+ ): Promise<DetectedFace[]> {
7
+ throw new Error(
8
+ "'react-native-facefusion' is only supported on native platforms."
9
+ );
10
+ }
@@ -0,0 +1,15 @@
1
+ import Facefusion from './NativeFacefusion';
2
+ import type { DetectedFace, DetectOptions } from './NativeFacefusion';
3
+
4
+ /**
5
+ * Every face detected in the target at `targetPath` — a photo, or a video (its first
6
+ * frame, already upright) — for a UI to let the user pick one before swapping — pass the
7
+ * chosen face's box back as `targetFaceBox` in `SwapOptions`. Does not swap or modify
8
+ * anything.
9
+ */
10
+ export function detectTargetFaces(
11
+ targetPath: string,
12
+ options?: DetectOptions
13
+ ): Promise<DetectedFace[]> {
14
+ return Facefusion.detectTargetFaces(targetPath, options);
15
+ }
@@ -0,0 +1,10 @@
1
+ import type { DetectedFace, DetectOptions } from './NativeFacefusion';
2
+
3
+ export function detectTargetFaces(
4
+ _targetPath: string,
5
+ _options?: DetectOptions
6
+ ): Promise<DetectedFace[]> {
7
+ throw new Error(
8
+ "'react-native-facefusion' is only supported on native platforms."
9
+ );
10
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,25 @@
1
+ export { probeDevice } from './probeDevice';
2
+ export {
3
+ getModelStatus,
4
+ downloadModels,
5
+ cancelModelDownload,
6
+ onModelDownloadProgress,
7
+ } from './models';
8
+ export { swapPhoto } from './swapPhoto';
9
+ export { swapVideo, cancelVideoSwap, onVideoSwapProgress } from './swapVideo';
10
+ export { detectSourceFaces } from './detectSourceFaces';
11
+ export { detectTargetFaces } from './detectTargetFaces';
12
+ export { saveToGallery } from './saveToGallery';
13
+ export { FacefusionPreview } from './FacefusionPreview';
14
+ export type { FacefusionPreviewProps } from './FacefusionPreview';
15
+ export type {
16
+ DeviceProbeResult,
17
+ ModelStatus,
18
+ ModelDownloadProgress,
19
+ SwapOptions,
20
+ DetectOptions,
21
+ SwapPhotoResult,
22
+ SwapVideoResult,
23
+ VideoSwapProgress,
24
+ DetectedFace,
25
+ } from './NativeFacefusion';
@@ -0,0 +1,45 @@
1
+ import Facefusion from './NativeFacefusion';
2
+ import type { EventSubscription } from 'react-native';
3
+ import type { ModelStatus, ModelDownloadProgress } from './NativeFacefusion';
4
+
5
+ /**
6
+ * What is on disk and whether a swap could run right now. Reads the filesystem only —
7
+ * no network, cheap enough to call whenever a screen appears.
8
+ */
9
+ export function getModelStatus(): Promise<ModelStatus> {
10
+ return Facefusion.getModelStatus();
11
+ }
12
+
13
+ /**
14
+ * Download every model this device still needs, resolving with the resulting status.
15
+ *
16
+ * ~317 MB on a fresh install, and minutes on a phone connection. Resumable: an interrupted
17
+ * run leaves `.part` files and calling this again continues from where it stopped, so a
18
+ * retry after a dropped connection is cheap. Files already present and the right size are
19
+ * skipped, so calling it when everything is downloaded costs one manifest request.
20
+ *
21
+ * Rejects with `E_CANCELLED` after {@link cancelModelDownload}, `E_BUSY` if a download is
22
+ * already running, and `E_DOWNLOAD` naming the file that failed otherwise.
23
+ */
24
+ export function downloadModels(): Promise<ModelStatus> {
25
+ return Facefusion.downloadModels();
26
+ }
27
+
28
+ /**
29
+ * Stop the download in flight. The promise from {@link downloadModels} rejects with
30
+ * `E_CANCELLED`; partial files are kept for a later resume.
31
+ */
32
+ export function cancelModelDownload(): void {
33
+ Facefusion.cancelModelDownload();
34
+ }
35
+
36
+ /**
37
+ * Subscribe to download progress. Emits about four times a second while a download runs.
38
+ *
39
+ * Returns the subscription — call `.remove()` on unmount.
40
+ */
41
+ export function onModelDownloadProgress(
42
+ listener: (progress: ModelDownloadProgress) => void
43
+ ): EventSubscription {
44
+ return Facefusion.onModelDownloadProgress(listener);
45
+ }
package/src/models.tsx ADDED
@@ -0,0 +1,23 @@
1
+ import type { EventSubscription } from 'react-native';
2
+ import type { ModelStatus, ModelDownloadProgress } from './NativeFacefusion';
3
+
4
+ const unsupported = () =>
5
+ new Error("'react-native-facefusion' is only supported on native platforms.");
6
+
7
+ export function getModelStatus(): Promise<ModelStatus> {
8
+ throw unsupported();
9
+ }
10
+
11
+ export function downloadModels(): Promise<ModelStatus> {
12
+ throw unsupported();
13
+ }
14
+
15
+ export function cancelModelDownload(): void {
16
+ throw unsupported();
17
+ }
18
+
19
+ export function onModelDownloadProgress(
20
+ _listener: (progress: ModelDownloadProgress) => void
21
+ ): EventSubscription {
22
+ throw unsupported();
23
+ }
@@ -0,0 +1,13 @@
1
+ import Facefusion from './NativeFacefusion';
2
+ import type { DeviceProbeResult } from './NativeFacefusion';
3
+
4
+ /**
5
+ * Ask the device which NPU it has and which model tier it needs.
6
+ *
7
+ * Cheap enough to call on startup, and safe on hardware that has no NPU at all — it
8
+ * resolves with `ok: false` rather than rejecting. It rejects only when the native
9
+ * library itself is missing or unloadable.
10
+ */
11
+ export function probeDevice(): Promise<DeviceProbeResult> {
12
+ return Facefusion.probeDevice();
13
+ }
@@ -0,0 +1,7 @@
1
+ import type { DeviceProbeResult } from './NativeFacefusion';
2
+
3
+ export function probeDevice(): Promise<DeviceProbeResult> {
4
+ throw new Error(
5
+ "'react-native-facefusion' is only supported on native platforms."
6
+ );
7
+ }
@@ -0,0 +1,16 @@
1
+ import Facefusion from './NativeFacefusion';
2
+
3
+ /**
4
+ * Copies a `swapPhoto`/`swapVideo` output into the system's Photos/Gallery app, so it
5
+ * survives beyond this app's own private storage. Returns the resulting `content://` URI.
6
+ *
7
+ * `mimeType` must be `image/*` or `video/*` — pass the real one, there's no safe way to
8
+ * guess it from a path alone. No storage permission is needed on Android.
9
+ */
10
+ export function saveToGallery(
11
+ path: string,
12
+ mimeType: string,
13
+ displayName?: string
14
+ ): Promise<string> {
15
+ return Facefusion.saveToGallery(path, mimeType, displayName);
16
+ }
@@ -0,0 +1,9 @@
1
+ export function saveToGallery(
2
+ _path: string,
3
+ _mimeType: string,
4
+ _displayName?: string
5
+ ): Promise<string> {
6
+ throw new Error(
7
+ "'react-native-facefusion' is only supported on native platforms."
8
+ );
9
+ }
@@ -0,0 +1,20 @@
1
+ import Facefusion from './NativeFacefusion';
2
+ import type { SwapOptions, SwapPhotoResult } from './NativeFacefusion';
3
+
4
+ /**
5
+ * Swaps the face from `sourcePath` into every face found in `targetPath`, writing the
6
+ * result to `outputPath`.
7
+ *
8
+ * Both images are decoded and re-encoded natively — nothing but paths and options crosses
9
+ * the JS bridge. Rejects with `E_BUSY` if another swap (or, from Phase 7, a video job) is
10
+ * already running, `E_MODELS` if the required models are not downloaded yet, and `E_SWAP`
11
+ * for anything else, naming what failed.
12
+ */
13
+ export function swapPhoto(
14
+ sourcePath: string,
15
+ targetPath: string,
16
+ outputPath: string,
17
+ options?: SwapOptions
18
+ ): Promise<SwapPhotoResult> {
19
+ return Facefusion.swapPhoto(sourcePath, targetPath, outputPath, options);
20
+ }
@@ -0,0 +1,12 @@
1
+ import type { SwapOptions, SwapPhotoResult } from './NativeFacefusion';
2
+
3
+ export function swapPhoto(
4
+ _sourcePath: string,
5
+ _targetPath: string,
6
+ _outputPath: string,
7
+ _options?: SwapOptions
8
+ ): Promise<SwapPhotoResult> {
9
+ throw new Error(
10
+ "'react-native-facefusion' is only supported on native platforms."
11
+ );
12
+ }
@@ -0,0 +1,40 @@
1
+ import Facefusion from './NativeFacefusion';
2
+ import type { EventSubscription } from 'react-native';
3
+ import type {
4
+ SwapOptions,
5
+ SwapVideoResult,
6
+ VideoSwapProgress,
7
+ } from './NativeFacefusion';
8
+
9
+ /**
10
+ * Swaps the face from `sourcePath` into every frame of the video at `targetPath`, writing
11
+ * the result to `outputPath`.
12
+ *
13
+ * Runs behind a foreground service — Android kills a plain background process partway
14
+ * through a job this long. Rejects with `E_BUSY` if a photo or another video swap is
15
+ * already running, `E_MODELS` if the required models are not downloaded yet, `E_CANCELLED`
16
+ * if `cancelVideoSwap()` was called, and `E_SWAP` for anything else, naming what failed.
17
+ */
18
+ export function swapVideo(
19
+ sourcePath: string,
20
+ targetPath: string,
21
+ outputPath: string,
22
+ options?: SwapOptions
23
+ ): Promise<SwapVideoResult> {
24
+ return Facefusion.swapVideo(sourcePath, targetPath, outputPath, options);
25
+ }
26
+
27
+ /** Asks the video swap in flight to stop. Fire-and-forget — see `swapVideo`'s doc. */
28
+ export function cancelVideoSwap(): void {
29
+ Facefusion.cancelVideoSwap();
30
+ }
31
+
32
+ /**
33
+ * Subscribe to video swap progress. Returns the subscription — call `.remove()` on
34
+ * unmount.
35
+ */
36
+ export function onVideoSwapProgress(
37
+ listener: (progress: VideoSwapProgress) => void
38
+ ): EventSubscription {
39
+ return Facefusion.onVideoSwapProgress(listener);
40
+ }
@@ -0,0 +1,31 @@
1
+ import type { EventSubscription } from 'react-native';
2
+ import type {
3
+ SwapOptions,
4
+ SwapVideoResult,
5
+ VideoSwapProgress,
6
+ } from './NativeFacefusion';
7
+
8
+ export function swapVideo(
9
+ _sourcePath: string,
10
+ _targetPath: string,
11
+ _outputPath: string,
12
+ _options?: SwapOptions
13
+ ): Promise<SwapVideoResult> {
14
+ throw new Error(
15
+ "'react-native-facefusion' is only supported on native platforms."
16
+ );
17
+ }
18
+
19
+ export function cancelVideoSwap(): void {
20
+ throw new Error(
21
+ "'react-native-facefusion' is only supported on native platforms."
22
+ );
23
+ }
24
+
25
+ export function onVideoSwapProgress(
26
+ _listener: (progress: VideoSwapProgress) => void
27
+ ): EventSubscription {
28
+ throw new Error(
29
+ "'react-native-facefusion' is only supported on native platforms."
30
+ );
31
+ }