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.
- package/LICENSE +20 -0
- package/README.md +259 -0
- package/android/build.gradle +129 -0
- package/android/src/main/AndroidManifest.xml +66 -0
- package/android/src/main/cpp/CMakeLists.txt +73 -0
- package/android/src/main/java/com/facefusion/BitmapDecode.kt +112 -0
- package/android/src/main/java/com/facefusion/ContentGate.kt +86 -0
- package/android/src/main/java/com/facefusion/DeviceProbe.kt +158 -0
- package/android/src/main/java/com/facefusion/FaceCrop.kt +57 -0
- package/android/src/main/java/com/facefusion/FacefusionModule.kt +396 -0
- package/android/src/main/java/com/facefusion/FacefusionPackage.kt +37 -0
- package/android/src/main/java/com/facefusion/FacefusionPreviewView.kt +30 -0
- package/android/src/main/java/com/facefusion/FacefusionPreviewViewManager.kt +25 -0
- package/android/src/main/java/com/facefusion/GallerySave.kt +82 -0
- package/android/src/main/java/com/facefusion/ModelDownload.kt +351 -0
- package/android/src/main/java/com/facefusion/ModelPaths.kt +120 -0
- package/android/src/main/java/com/facefusion/PhotoSwap.kt +125 -0
- package/android/src/main/java/com/facefusion/PipeGuard.kt +74 -0
- package/android/src/main/java/com/facefusion/PreviewSurfaceHolder.kt +104 -0
- package/android/src/main/java/com/facefusion/SourceFaces.kt +125 -0
- package/android/src/main/java/com/facefusion/SwapConfig.kt +31 -0
- package/android/src/main/java/com/facefusion/TargetFaces.kt +88 -0
- package/android/src/main/java/com/facefusion/VideoSwap.kt +501 -0
- package/android/src/main/java/com/facefusion/VideoSwapService.kt +85 -0
- package/android/src/main/java/com/facefusion/mobile/NativePipe.kt +192 -0
- package/lib/module/FacefusionPreview.js +6 -0
- package/lib/module/FacefusionPreview.js.map +1 -0
- package/lib/module/FacefusionPreview.native.js +15 -0
- package/lib/module/FacefusionPreview.native.js.map +1 -0
- package/lib/module/NativeFacefusion.js +53 -0
- package/lib/module/NativeFacefusion.js.map +1 -0
- package/lib/module/detectSourceFaces.js +6 -0
- package/lib/module/detectSourceFaces.js.map +1 -0
- package/lib/module/detectSourceFaces.native.js +15 -0
- package/lib/module/detectSourceFaces.native.js.map +1 -0
- package/lib/module/detectTargetFaces.js +6 -0
- package/lib/module/detectTargetFaces.js.map +1 -0
- package/lib/module/detectTargetFaces.native.js +13 -0
- package/lib/module/detectTargetFaces.native.js.map +1 -0
- package/lib/module/index.js +11 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/models.js +16 -0
- package/lib/module/models.js.map +1 -0
- package/lib/module/models.native.js +43 -0
- package/lib/module/models.native.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/probeDevice.js +6 -0
- package/lib/module/probeDevice.js.map +1 -0
- package/lib/module/probeDevice.native.js +14 -0
- package/lib/module/probeDevice.native.js.map +1 -0
- package/lib/module/saveToGallery.js +6 -0
- package/lib/module/saveToGallery.js.map +1 -0
- package/lib/module/saveToGallery.native.js +15 -0
- package/lib/module/saveToGallery.native.js.map +1 -0
- package/lib/module/swapPhoto.js +6 -0
- package/lib/module/swapPhoto.js.map +1 -0
- package/lib/module/swapPhoto.native.js +16 -0
- package/lib/module/swapPhoto.native.js.map +1 -0
- package/lib/module/swapVideo.js +12 -0
- package/lib/module/swapVideo.js.map +1 -0
- package/lib/module/swapVideo.native.js +29 -0
- package/lib/module/swapVideo.native.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/FacefusionPreview.d.ts +4 -0
- package/lib/typescript/src/FacefusionPreview.d.ts.map +1 -0
- package/lib/typescript/src/FacefusionPreview.native.d.ts +142 -0
- package/lib/typescript/src/FacefusionPreview.native.d.ts.map +1 -0
- package/lib/typescript/src/NativeFacefusion.d.ts +291 -0
- package/lib/typescript/src/NativeFacefusion.d.ts.map +1 -0
- package/lib/typescript/src/detectSourceFaces.d.ts +3 -0
- package/lib/typescript/src/detectSourceFaces.d.ts.map +1 -0
- package/lib/typescript/src/detectSourceFaces.native.d.ts +11 -0
- package/lib/typescript/src/detectSourceFaces.native.d.ts.map +1 -0
- package/lib/typescript/src/detectTargetFaces.d.ts +3 -0
- package/lib/typescript/src/detectTargetFaces.d.ts.map +1 -0
- package/lib/typescript/src/detectTargetFaces.native.d.ts +9 -0
- package/lib/typescript/src/detectTargetFaces.native.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +11 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/models.d.ts +7 -0
- package/lib/typescript/src/models.d.ts.map +1 -0
- package/lib/typescript/src/models.native.d.ts +31 -0
- package/lib/typescript/src/models.native.d.ts.map +1 -0
- package/lib/typescript/src/probeDevice.d.ts +3 -0
- package/lib/typescript/src/probeDevice.d.ts.map +1 -0
- package/lib/typescript/src/probeDevice.native.d.ts +10 -0
- package/lib/typescript/src/probeDevice.native.d.ts.map +1 -0
- package/lib/typescript/src/saveToGallery.d.ts +2 -0
- package/lib/typescript/src/saveToGallery.d.ts.map +1 -0
- package/lib/typescript/src/saveToGallery.native.d.ts +9 -0
- package/lib/typescript/src/saveToGallery.native.d.ts.map +1 -0
- package/lib/typescript/src/swapPhoto.d.ts +3 -0
- package/lib/typescript/src/swapPhoto.d.ts.map +1 -0
- package/lib/typescript/src/swapPhoto.native.d.ts +12 -0
- package/lib/typescript/src/swapPhoto.native.d.ts.map +1 -0
- package/lib/typescript/src/swapVideo.d.ts +6 -0
- package/lib/typescript/src/swapVideo.d.ts.map +1 -0
- package/lib/typescript/src/swapVideo.native.d.ts +20 -0
- package/lib/typescript/src/swapVideo.native.d.ts.map +1 -0
- package/package.json +132 -0
- package/patches/ffjni-analyse-faces.patch +63 -0
- package/scripts/check-tarball.sh +68 -0
- package/scripts/fetch-upstream.sh +91 -0
- package/src/FacefusionPreview.native.tsx +18 -0
- package/src/FacefusionPreview.tsx +9 -0
- package/src/NativeFacefusion.ts +324 -0
- package/src/detectSourceFaces.native.tsx +17 -0
- package/src/detectSourceFaces.tsx +10 -0
- package/src/detectTargetFaces.native.tsx +15 -0
- package/src/detectTargetFaces.tsx +10 -0
- package/src/index.tsx +25 -0
- package/src/models.native.tsx +45 -0
- package/src/models.tsx +23 -0
- package/src/probeDevice.native.tsx +13 -0
- package/src/probeDevice.tsx +7 -0
- package/src/saveToGallery.native.tsx +16 -0
- package/src/saveToGallery.tsx +9 -0
- package/src/swapPhoto.native.tsx +20 -0
- package/src/swapPhoto.tsx +12 -0
- package/src/swapVideo.native.tsx +40 -0
- package/src/swapVideo.tsx +31 -0
- package/third_party/facefusion-mobile/NOTICE +49 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probeDevice.native.d.ts","sourceRoot":"","sources":["../../../src/probeDevice.native.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAoB,CAAC;AAE5D;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAExD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saveToGallery.d.ts","sourceRoot":"","sources":["../../../src/saveToGallery.tsx"],"names":[],"mappings":"AAAA,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAIjB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copies a `swapPhoto`/`swapVideo` output into the system's Photos/Gallery app, so it
|
|
3
|
+
* survives beyond this app's own private storage. Returns the resulting `content://` URI.
|
|
4
|
+
*
|
|
5
|
+
* `mimeType` must be `image/*` or `video/*` — pass the real one, there's no safe way to
|
|
6
|
+
* guess it from a path alone. No storage permission is needed on Android.
|
|
7
|
+
*/
|
|
8
|
+
export declare function saveToGallery(path: string, mimeType: string, displayName?: string): Promise<string>;
|
|
9
|
+
//# sourceMappingURL=saveToGallery.native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saveToGallery.native.d.ts","sourceRoot":"","sources":["../../../src/saveToGallery.native.tsx"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CAEjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"swapPhoto.d.ts","sourceRoot":"","sources":["../../../src/swapPhoto.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,uBAAoB,CAAC;AAEvE,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,WAAW,GACrB,OAAO,CAAC,eAAe,CAAC,CAI1B"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SwapOptions, SwapPhotoResult } from './NativeFacefusion.js';
|
|
2
|
+
/**
|
|
3
|
+
* Swaps the face from `sourcePath` into every face found in `targetPath`, writing the
|
|
4
|
+
* result to `outputPath`.
|
|
5
|
+
*
|
|
6
|
+
* Both images are decoded and re-encoded natively — nothing but paths and options crosses
|
|
7
|
+
* the JS bridge. Rejects with `E_BUSY` if another swap (or, from Phase 7, a video job) is
|
|
8
|
+
* already running, `E_MODELS` if the required models are not downloaded yet, and `E_SWAP`
|
|
9
|
+
* for anything else, naming what failed.
|
|
10
|
+
*/
|
|
11
|
+
export declare function swapPhoto(sourcePath: string, targetPath: string, outputPath: string, options?: SwapOptions): Promise<SwapPhotoResult>;
|
|
12
|
+
//# sourceMappingURL=swapPhoto.native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"swapPhoto.native.d.ts","sourceRoot":"","sources":["../../../src/swapPhoto.native.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,uBAAoB,CAAC;AAEvE;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CACvB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,eAAe,CAAC,CAE1B"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EventSubscription } from 'react-native';
|
|
2
|
+
import type { SwapOptions, SwapVideoResult, VideoSwapProgress } from './NativeFacefusion.js';
|
|
3
|
+
export declare function swapVideo(_sourcePath: string, _targetPath: string, _outputPath: string, _options?: SwapOptions): Promise<SwapVideoResult>;
|
|
4
|
+
export declare function cancelVideoSwap(): void;
|
|
5
|
+
export declare function onVideoSwapProgress(_listener: (progress: VideoSwapProgress) => void): EventSubscription;
|
|
6
|
+
//# sourceMappingURL=swapVideo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"swapVideo.d.ts","sourceRoot":"","sources":["../../../src/swapVideo.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,iBAAiB,EAClB,MAAM,uBAAoB,CAAC;AAE5B,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,WAAW,GACrB,OAAO,CAAC,eAAe,CAAC,CAI1B;AAED,wBAAgB,eAAe,IAAI,IAAI,CAItC;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,GAC/C,iBAAiB,CAInB"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { EventSubscription } from 'react-native';
|
|
2
|
+
import type { SwapOptions, SwapVideoResult, VideoSwapProgress } from './NativeFacefusion.js';
|
|
3
|
+
/**
|
|
4
|
+
* Swaps the face from `sourcePath` into every frame of the video at `targetPath`, writing
|
|
5
|
+
* the result to `outputPath`.
|
|
6
|
+
*
|
|
7
|
+
* Runs behind a foreground service — Android kills a plain background process partway
|
|
8
|
+
* through a job this long. Rejects with `E_BUSY` if a photo or another video swap is
|
|
9
|
+
* already running, `E_MODELS` if the required models are not downloaded yet, `E_CANCELLED`
|
|
10
|
+
* if `cancelVideoSwap()` was called, and `E_SWAP` for anything else, naming what failed.
|
|
11
|
+
*/
|
|
12
|
+
export declare function swapVideo(sourcePath: string, targetPath: string, outputPath: string, options?: SwapOptions): Promise<SwapVideoResult>;
|
|
13
|
+
/** Asks the video swap in flight to stop. Fire-and-forget — see `swapVideo`'s doc. */
|
|
14
|
+
export declare function cancelVideoSwap(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Subscribe to video swap progress. Returns the subscription — call `.remove()` on
|
|
17
|
+
* unmount.
|
|
18
|
+
*/
|
|
19
|
+
export declare function onVideoSwapProgress(listener: (progress: VideoSwapProgress) => void): EventSubscription;
|
|
20
|
+
//# sourceMappingURL=swapVideo.native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"swapVideo.native.d.ts","sourceRoot":"","sources":["../../../src/swapVideo.native.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,iBAAiB,EAClB,MAAM,uBAAoB,CAAC;AAE5B;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CACvB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,eAAe,CAAC,CAE1B;AAED,sFAAsF;AACtF,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,GAC9C,iBAAiB,CAEnB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-facefusion",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React Native TurboModule for on-device face swapping on Qualcomm Hexagon NPUs. Runs fully offline on the phone, no server and no uploads.",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"react-native-facefusion-source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"android",
|
|
19
|
+
"scripts/fetch-upstream.sh",
|
|
20
|
+
"scripts/check-tarball.sh",
|
|
21
|
+
"patches",
|
|
22
|
+
"third_party/facefusion-mobile/NOTICE",
|
|
23
|
+
"!android/src/main/jniLibs",
|
|
24
|
+
"!android/src/main/cpp/include/QNN",
|
|
25
|
+
"!third_party/facefusion-mobile/cpp",
|
|
26
|
+
"!android/build",
|
|
27
|
+
"!android/gradle",
|
|
28
|
+
"!android/gradlew",
|
|
29
|
+
"!android/gradlew.bat",
|
|
30
|
+
"!android/local.properties",
|
|
31
|
+
"!**/__tests__",
|
|
32
|
+
"!**/__fixtures__",
|
|
33
|
+
"!**/__mocks__",
|
|
34
|
+
"!**/.*"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"example": "npm run --workspace=example",
|
|
38
|
+
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
39
|
+
"prepare": "bob build",
|
|
40
|
+
"typecheck": "tsc",
|
|
41
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
42
|
+
"prepublishOnly": "./scripts/check-tarball.sh"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"react-native",
|
|
46
|
+
"android",
|
|
47
|
+
"face-swap",
|
|
48
|
+
"npu",
|
|
49
|
+
"qualcomm",
|
|
50
|
+
"on-device-ml"
|
|
51
|
+
],
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/shipitsaad/facefusion-rn.git"
|
|
55
|
+
},
|
|
56
|
+
"author": "Saad <saad.saad737@gmail.com> (https://github.com/shipitsaad)",
|
|
57
|
+
"license": "MIT",
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/shipitsaad/facefusion-rn/issues"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://github.com/shipitsaad/facefusion-rn#readme",
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"registry": "https://registry.npmjs.org/"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@eslint/compat": "^2.1.0",
|
|
67
|
+
"@eslint/eslintrc": "^3.3.5",
|
|
68
|
+
"@eslint/js": "^9.39.5",
|
|
69
|
+
"@react-native/babel-preset": "0.85.0",
|
|
70
|
+
"@react-native/eslint-config": "0.85.0",
|
|
71
|
+
"@types/react": "^19.2.0",
|
|
72
|
+
"del-cli": "^7.0.0",
|
|
73
|
+
"eslint": "^9.39.4",
|
|
74
|
+
"eslint-config-prettier": "^10.1.8",
|
|
75
|
+
"eslint-plugin-ft-flow": "^3.0.11",
|
|
76
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
77
|
+
"prettier": "^3.8.3",
|
|
78
|
+
"react": "19.2.3",
|
|
79
|
+
"react-native": "0.85.0",
|
|
80
|
+
"react-native-builder-bob": "^0.43.0",
|
|
81
|
+
"turbo": "^2.9.16",
|
|
82
|
+
"typescript": "^6.0.3"
|
|
83
|
+
},
|
|
84
|
+
"peerDependencies": {
|
|
85
|
+
"react": "*",
|
|
86
|
+
"react-native": "*"
|
|
87
|
+
},
|
|
88
|
+
"workspaces": [
|
|
89
|
+
"example"
|
|
90
|
+
],
|
|
91
|
+
"react-native-builder-bob": {
|
|
92
|
+
"source": "src",
|
|
93
|
+
"output": "lib",
|
|
94
|
+
"targets": [
|
|
95
|
+
[
|
|
96
|
+
"module",
|
|
97
|
+
{
|
|
98
|
+
"esm": true
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"typescript",
|
|
103
|
+
{
|
|
104
|
+
"project": "tsconfig.build.json"
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
"codegenConfig": {
|
|
110
|
+
"name": "FacefusionSpec",
|
|
111
|
+
"type": "modules",
|
|
112
|
+
"jsSrcsDir": "src",
|
|
113
|
+
"android": {
|
|
114
|
+
"javaPackageName": "com.facefusion"
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"prettier": {
|
|
118
|
+
"quoteProps": "consistent",
|
|
119
|
+
"singleQuote": true,
|
|
120
|
+
"tabWidth": 2,
|
|
121
|
+
"trailingComma": "es5",
|
|
122
|
+
"useTabs": false
|
|
123
|
+
},
|
|
124
|
+
"create-react-native-library": {
|
|
125
|
+
"type": "turbo-module",
|
|
126
|
+
"languages": "kotlin-objc",
|
|
127
|
+
"tools": [
|
|
128
|
+
"eslint"
|
|
129
|
+
],
|
|
130
|
+
"version": "0.63.0"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--- ffjni.cpp
|
|
2
|
+
+++ ffjni.cpp
|
|
3
|
+
@@ -236,6 +236,60 @@
|
|
4
|
+
if (!faces.empty())
|
|
5
|
+
env->SetByteArrayRegion(jBgr, 0, (jsize)img.data.size(), (const jbyte*)img.data.data());
|
|
6
|
+
return (jint)faces.size();
|
|
7
|
+
+}
|
|
8
|
+
+
|
|
9
|
+
+// Ours, not upstream's -- see third_party/facefusion-mobile/NOTICE and
|
|
10
|
+
+// docs/02-upstream.md "Patches". Applied by scripts/fetch-upstream.sh from
|
|
11
|
+
+// patches/ffjni-analyse-faces.patch after every re-sync.
|
|
12
|
+
+//
|
|
13
|
+
+// Detects every face in an image without swapping: box + confidence only, no
|
|
14
|
+
+// embeddings. Reuses the already-public Pipeline::analyse() and the already-warm
|
|
15
|
+
+// g_pipe -- no new pipeline state, no change to any existing function. Lets a
|
|
16
|
+
+// caller with a multi-face source photo see what's in it before deciding which one
|
|
17
|
+
+// becomes the identity; the caller commits a choice by cropping to that face's box
|
|
18
|
+
+// and calling the existing, unmodified setSource() on the crop, not by any new
|
|
19
|
+
+// entry point here.
|
|
20
|
+
+JNIEXPORT jfloatArray JNICALL
|
|
21
|
+
+Java_com_facefusion_mobile_NativePipe_analyseFaces(JNIEnv* env, jclass, jbyteArray jBgr,
|
|
22
|
+
+ jint w, jint h) {
|
|
23
|
+
+ if (!g_pipe) { g_err = "pipeline not initialised"; return nullptr; }
|
|
24
|
+
+ ffcv::Image img(w, h, 3);
|
|
25
|
+
+ if ((size_t)env->GetArrayLength(jBgr) != img.data.size()) {
|
|
26
|
+
+ g_err = "analyseFaces: frame is not w*h*3 bytes";
|
|
27
|
+
+ return nullptr;
|
|
28
|
+
+ }
|
|
29
|
+
+ env->GetByteArrayRegion(jBgr, 0, (jsize)img.data.size(), (jbyte*)img.data.data());
|
|
30
|
+
+
|
|
31
|
+
+ // A detector graph that FAILED TO EXECUTE and a frame with genuinely no face both
|
|
32
|
+
+ // come back as an empty vector. Pipeline::setSource carries a comment saying exactly
|
|
33
|
+
+ // that -- "reporting the second when it was the first sends the next hour in the wrong
|
|
34
|
+
+ // place" -- and the first version of this export ignored it, returning an empty array
|
|
35
|
+
+ // either way. A real detector failure therefore reached the user as "no faces found",
|
|
36
|
+
+ // with nothing in logcat, which is precisely what happened on a v69 phone on
|
|
37
|
+
+ // 2026-09-14 and cost a remote debugging round trip.
|
|
38
|
+
+ //
|
|
39
|
+
+ // analyse() does not clear error() on entry, so a non-empty string is not on its own
|
|
40
|
+
+ // proof that THIS call failed; compare against what was already there.
|
|
41
|
+
+ const std::string errBefore = g_pipe->error();
|
|
42
|
+
+ auto faces = g_pipe->analyse(img);
|
|
43
|
+
+ const std::string& errAfter = g_pipe->error();
|
|
44
|
+
+ if (faces.empty() && !errAfter.empty() && errAfter != errBefore) {
|
|
45
|
+
+ g_err = errAfter;
|
|
46
|
+
+ return nullptr;
|
|
47
|
+
+ }
|
|
48
|
+
+
|
|
49
|
+
+ std::vector<float> out;
|
|
50
|
+
+ out.reserve(faces.size() * 5);
|
|
51
|
+
+ for (auto& f : faces) {
|
|
52
|
+
+ out.push_back(f.box[0]);
|
|
53
|
+
+ out.push_back(f.box[1]);
|
|
54
|
+
+ out.push_back(f.box[2]);
|
|
55
|
+
+ out.push_back(f.box[3]);
|
|
56
|
+
+ out.push_back(f.detScore);
|
|
57
|
+
+ }
|
|
58
|
+
+ jfloatArray arr = env->NewFloatArray((jsize)out.size());
|
|
59
|
+
+ env->SetFloatArrayRegion(arr, 0, (jsize)out.size(), out.data());
|
|
60
|
+
+ return arr;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Bitmap ARGB_8888 ints -> packed BGR bytes. */
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Refuses to publish a tarball containing Qualcomm's QAIRT runtime, its proprietary
|
|
4
|
+
# headers, upstream's unlicensed C++, or a model binary.
|
|
5
|
+
#
|
|
6
|
+
# This runs as `prepublishOnly`, ON THE PUBLISHING MACHINE, and that placement is the
|
|
7
|
+
# whole point. CI has a step that greps the packed tarball too -- but a CI checkout
|
|
8
|
+
# cannot contain any of these files in the first place (they are gitignored and fetched
|
|
9
|
+
# separately), so the grep there passes trivially and proves nothing. The only machine
|
|
10
|
+
# where the bytes actually exist is a developer's, which is the only machine that ever
|
|
11
|
+
# runs `npm publish`. A guard that cannot fire where the risk lives is decoration.
|
|
12
|
+
#
|
|
13
|
+
# See docs/05-licensing.md: redistribution of the QAIRT runtime is permitted only "as
|
|
14
|
+
# incorporated in Your software application", never "on a standalone basis" -- an APK
|
|
15
|
+
# may ship it, an npm package may not.
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
|
19
|
+
|
|
20
|
+
echo "prepublish: auditing the tarball..."
|
|
21
|
+
|
|
22
|
+
# Build the real tarball and read it with tar, rather than parsing `npm pack --json`:
|
|
23
|
+
# `npm pack` re-runs `prepare` (bob build), which prints lines like
|
|
24
|
+
# "[typescript] Cleaning up previous build" to STDOUT, ahead of the JSON. Any attempt to
|
|
25
|
+
# slice the JSON out of that stream picks up bob's bracket instead of the array's.
|
|
26
|
+
# The bytes on disk are the thing being audited anyway.
|
|
27
|
+
tmp="$(mktemp -d)"
|
|
28
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
29
|
+
npm pack --pack-destination "$tmp" >/dev/null 2>&1
|
|
30
|
+
tgz="$(ls "$tmp"/*.tgz 2>/dev/null | head -1)"
|
|
31
|
+
if [ -z "$tgz" ]; then
|
|
32
|
+
echo "prepublish: npm pack produced no tarball" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
files="$(tar -tzf "$tgz" | sed 's|^package/||' | grep -v '/$')"
|
|
36
|
+
|
|
37
|
+
banned="$(printf '%s\n' "$files" | grep -Ei 'libQnn|\.so$|\.bin$|include/QNN/|hexagon|third_party/facefusion-mobile/cpp/' || true)"
|
|
38
|
+
if [ -n "$banned" ]; then
|
|
39
|
+
echo >&2
|
|
40
|
+
echo "PUBLISH BLOCKED -- the tarball contains files that must never be redistributed:" >&2
|
|
41
|
+
printf ' %s\n' $banned >&2
|
|
42
|
+
echo >&2
|
|
43
|
+
echo "See docs/05-licensing.md. Check the \"files\" array in package.json:" >&2
|
|
44
|
+
echo "npm's files list OVERRIDES .gitignore, so being gitignored is not protection." >&2
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Clean is not enough; it also has to be complete. These are what the README's install
|
|
49
|
+
# steps and the build actually need -- all four were missing once, which made the
|
|
50
|
+
# published package impossible to install.
|
|
51
|
+
required=(
|
|
52
|
+
package.json README.md LICENSE
|
|
53
|
+
scripts/fetch-upstream.sh
|
|
54
|
+
patches/ffjni-analyse-faces.patch
|
|
55
|
+
third_party/facefusion-mobile/NOTICE
|
|
56
|
+
lib/module/index.js
|
|
57
|
+
lib/typescript/src/index.d.ts
|
|
58
|
+
android/build.gradle
|
|
59
|
+
android/src/main/AndroidManifest.xml
|
|
60
|
+
android/src/main/cpp/CMakeLists.txt
|
|
61
|
+
)
|
|
62
|
+
missing=0
|
|
63
|
+
for f in "${required[@]}"; do
|
|
64
|
+
printf '%s\n' "$files" | grep -qxF "$f" || { echo "PUBLISH BLOCKED -- missing: $f" >&2; missing=1; }
|
|
65
|
+
done
|
|
66
|
+
[ "$missing" -eq 0 ] || exit 1
|
|
67
|
+
|
|
68
|
+
echo "prepublish: OK -- $(printf '%s\n' "$files" | wc -l | tr -d ' ') files, nothing proprietary, nothing missing."
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Places the upstream C++ engine into third_party/facefusion-mobile/cpp/.
|
|
4
|
+
#
|
|
5
|
+
# The files are NOT in this repository: upstream ships no LICENSE, so redistributing
|
|
6
|
+
# them is not permitted (docs/05-licensing.md §1). Cloning their public repo yourself
|
|
7
|
+
# is, which is all this script does.
|
|
8
|
+
#
|
|
9
|
+
# Pinned to a commit on purpose. Upstream moves daily, and a re-sync must be a
|
|
10
|
+
# deliberate act with a diff review, not something a build silently picks up.
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
REPO="https://github.com/AbrahamPaulJ/facefusion-mobile.git"
|
|
14
|
+
COMMIT="bf633acee030772deb2f2068866c0c0a17de24ba"
|
|
15
|
+
SRC_SUBDIR="work/android/app/src/main/cpp"
|
|
16
|
+
FILES=(ffjni.cpp ffpipe.cpp ffpipe.h ffcv.cpp ffcv.h ffqnn.cpp ffqnn.h)
|
|
17
|
+
|
|
18
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
19
|
+
DEST="$ROOT/third_party/facefusion-mobile/cpp"
|
|
20
|
+
|
|
21
|
+
TMP="$(mktemp -d)"
|
|
22
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
23
|
+
|
|
24
|
+
echo "Cloning $REPO @ ${COMMIT:0:7} ..."
|
|
25
|
+
git init -q "$TMP"
|
|
26
|
+
git -C "$TMP" remote add origin "$REPO"
|
|
27
|
+
git -C "$TMP" fetch -q --depth 1 origin "$COMMIT"
|
|
28
|
+
git -C "$TMP" checkout -q FETCH_HEAD
|
|
29
|
+
|
|
30
|
+
mkdir -p "$DEST"
|
|
31
|
+
for f in "${FILES[@]}"; do
|
|
32
|
+
cp "$TMP/$SRC_SUBDIR/$f" "$DEST/$f"
|
|
33
|
+
echo " $f"
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
# Verify against NOTICE, and FAIL on a mismatch. This used to print the hashes and tell
|
|
37
|
+
# the reader to compare them by eye, while the comment below claimed a check had happened
|
|
38
|
+
# -- a promise the script did not keep, found by installing the npm tarball into a clean
|
|
39
|
+
# app and reading what actually ran. A check nobody performs is not a check.
|
|
40
|
+
NOTICE="$ROOT/third_party/facefusion-mobile/NOTICE"
|
|
41
|
+
echo
|
|
42
|
+
if ! command -v shasum >/dev/null; then
|
|
43
|
+
echo "WARNING: shasum not found -- cannot verify the fetch against NOTICE." >&2
|
|
44
|
+
elif [ ! -f "$NOTICE" ]; then
|
|
45
|
+
echo "WARNING: $NOTICE is missing -- cannot verify the fetch." >&2
|
|
46
|
+
else
|
|
47
|
+
echo "Verifying against third_party/facefusion-mobile/NOTICE ..."
|
|
48
|
+
fail=0
|
|
49
|
+
for f in "${FILES[@]}"; do
|
|
50
|
+
got="$(shasum -a 256 "$DEST/$f" | cut -d' ' -f1)"
|
|
51
|
+
# NOTICE records each hash in a markdown table row naming the file: `cpp/ffjni.cpp`.
|
|
52
|
+
want="$(grep -F "cpp/$f\`" "$NOTICE" | grep -oE '[0-9a-f]{64}' | head -1)"
|
|
53
|
+
if [ -z "$want" ]; then
|
|
54
|
+
echo " ?? $f -- no hash recorded in NOTICE" >&2; fail=1
|
|
55
|
+
elif [ "$got" != "$want" ]; then
|
|
56
|
+
echo " !! $f -- MISMATCH" >&2
|
|
57
|
+
echo " expected $want" >&2
|
|
58
|
+
echo " got $got" >&2
|
|
59
|
+
fail=1
|
|
60
|
+
else
|
|
61
|
+
echo " ok $f"
|
|
62
|
+
fi
|
|
63
|
+
done
|
|
64
|
+
if [ "$fail" -ne 0 ]; then
|
|
65
|
+
echo >&2
|
|
66
|
+
echo "Upstream bytes do not match NOTICE. Refusing to patch or continue: a patch" >&2
|
|
67
|
+
echo "applied to unexpected sources would mask exactly this. If upstream moved on" >&2
|
|
68
|
+
echo "purpose, re-pin COMMIT and update NOTICE deliberately, with a diff review." >&2
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Patches, applied AFTER the hash check above -- that check verifies we got upstream's
|
|
74
|
+
# actual, unmodified bytes; patching is a separate, deliberate step on top of a known-
|
|
75
|
+
# good base, not something that should ever be allowed to mask a bad fetch.
|
|
76
|
+
#
|
|
77
|
+
# One patch exists today: patches/ffjni-analyse-faces.patch adds a single new JNI
|
|
78
|
+
# export to ffjni.cpp (Java_com_facefusion_mobile_NativePipe_analyseFaces) so a
|
|
79
|
+
# multi-face source photo can be shown to the user before a swap picks one. It changes
|
|
80
|
+
# no existing function. See docs/02-upstream.md "Patches" and NOTICE for why this is
|
|
81
|
+
# tracked here instead of edited in place: this script overwrites cpp/ from upstream
|
|
82
|
+
# every time it runs, so an in-place edit would not survive a re-fetch.
|
|
83
|
+
PATCH_DIR="$ROOT/patches"
|
|
84
|
+
if [ -d "$PATCH_DIR" ] && [ -n "$(ls -A "$PATCH_DIR"/*.patch 2>/dev/null)" ]; then
|
|
85
|
+
echo
|
|
86
|
+
echo "Applying patches..."
|
|
87
|
+
for p in "$PATCH_DIR"/*.patch; do
|
|
88
|
+
echo " $(basename "$p")"
|
|
89
|
+
patch "$DEST/ffjni.cpp" < "$p"
|
|
90
|
+
done
|
|
91
|
+
fi
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { requireNativeComponent } from 'react-native';
|
|
2
|
+
import type { ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export type FacefusionPreviewProps = ViewProps;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A live view of the frame [swapPhoto]/[swapVideo] just produced — the result the instant
|
|
8
|
+
* it's ready for a photo, and one frame at a time while a video swap is running.
|
|
9
|
+
*
|
|
10
|
+
* No prop carries pixels and none of this crosses the JS bridge: the native side
|
|
11
|
+
* (`PreviewSurfaceHolder.kt`) draws straight into this view's own `Surface` from whichever
|
|
12
|
+
* swap is running. Mount it, give it a size with `style`, and it shows whatever the native
|
|
13
|
+
* layer is currently drawing — there is nothing else to wire up. At most one swap runs at a
|
|
14
|
+
* time (`PipeGuard`), so at most one `<FacefusionPreview />` needs to be mounted to see it.
|
|
15
|
+
*/
|
|
16
|
+
export const FacefusionPreview = requireNativeComponent<FacefusionPreviewProps>(
|
|
17
|
+
'FacefusionPreviewView'
|
|
18
|
+
);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export type FacefusionPreviewProps = ViewProps;
|
|
4
|
+
|
|
5
|
+
export function FacefusionPreview(_props: FacefusionPreviewProps): never {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"'react-native-facefusion' is only supported on native platforms."
|
|
8
|
+
);
|
|
9
|
+
}
|