react-native-facefusion 0.1.0 → 0.1.1
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/README.md
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
# react-native-facefusion
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/react-native-facefusion)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](#requirements--read-this-first)
|
|
6
|
+
|
|
3
7
|
A React Native TurboModule that runs face swapping **entirely on the phone**, on
|
|
4
8
|
Qualcomm's Hexagon NPU. No server, no upload, no network call for inference — the
|
|
5
9
|
photo or video never leaves the device.
|
|
6
10
|
|
|
11
|
+
```ts
|
|
12
|
+
import { swapPhoto } from 'react-native-facefusion';
|
|
13
|
+
|
|
14
|
+
const result = await swapPhoto(sourcePath, targetPath, outputPath);
|
|
15
|
+
// { outputPath, faceCount, tier } -- inference ran on the phone's NPU, offline.
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That's the shape of it. Whether it'll actually run on a given phone depends on the
|
|
19
|
+
chip — read the requirements below before installing; they're not boilerplate.
|
|
20
|
+
|
|
7
21
|
## Requirements — read this first
|
|
8
22
|
|
|
9
23
|
- **Android only.** There is no iOS implementation. This runs on Qualcomm's Hexagon
|
|
@@ -22,22 +36,44 @@ photo or video never leaves the device.
|
|
|
22
36
|
- **A one-time native SDK step at install**, described below. It cannot be skipped
|
|
23
37
|
and it cannot be bundled into this package — see [Install](#install).
|
|
24
38
|
|
|
39
|
+
**Check a specific device before going further** — `probeDevice()` answers this at
|
|
40
|
+
runtime, so you don't have to guess from a spec sheet:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { probeDevice } from 'react-native-facefusion';
|
|
44
|
+
|
|
45
|
+
const device = await probeDevice();
|
|
46
|
+
console.log(device); // { ok, tier, tierChain, arch, vtcmMb, socModel, ... }
|
|
47
|
+
// ok: false means this exact phone cannot run the NPU path -- not a crash, an answer.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This still needs the SDK step below to build at all; it's here so you know what to
|
|
51
|
+
expect before spending time on that step.
|
|
52
|
+
|
|
25
53
|
## Install
|
|
26
54
|
|
|
27
|
-
|
|
55
|
+
Two manual steps, not one — a plain `npm install` gets you most of the way, not
|
|
56
|
+
all of it.
|
|
28
57
|
|
|
29
58
|
```sh
|
|
30
59
|
npm install react-native-facefusion
|
|
31
60
|
```
|
|
32
61
|
|
|
33
62
|
```sh
|
|
34
|
-
# 1
|
|
35
|
-
#
|
|
63
|
+
# Step 1 (fetching the upstream C++ engine) now runs AUTOMATICALLY as a postinstall --
|
|
64
|
+
# nothing to do here in the common case. It's shown below only for when it's needed by
|
|
65
|
+
# hand: your install had no network/git available (the postinstall warns rather than
|
|
66
|
+
# failing your whole `npm install` when that happens), or scripts are disabled
|
|
67
|
+
# (`npm ci --ignore-scripts`, some CI/lockfile-security setups). Not committed to this
|
|
68
|
+
# repo or to the npm package either way — see "Why the extra steps" below.
|
|
36
69
|
./node_modules/react-native-facefusion/scripts/fetch-upstream.sh
|
|
37
70
|
```
|
|
38
71
|
|
|
39
72
|
```sh
|
|
40
|
-
# 2
|
|
73
|
+
# Step 2 -- still manual, and can't be automated the same way: it's a real download
|
|
74
|
+
# from Qualcomm's own site, not a script fetching a public git repo.
|
|
75
|
+
#
|
|
76
|
+
# Get Qualcomm's QAIRT SDK (Community edition — a plain ZIP, no account needed):
|
|
41
77
|
# https://www.qualcomm.com/developer/software/qualcomm-ai-runtime-sdk-qairt
|
|
42
78
|
#
|
|
43
79
|
# Copy out of the ZIP into THIS PACKAGE's directory (not your app's) -- CMake resolves
|
|
@@ -60,10 +96,13 @@ npm install react-native-facefusion
|
|
|
60
96
|
# models arrive pre-compiled, so it is exactly the step this never performs.
|
|
61
97
|
```
|
|
62
98
|
|
|
63
|
-
**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
99
|
+
**Both steps write into `node_modules/`, so a fresh `npm install` or `npm ci` wipes
|
|
100
|
+
them out again.** Step 1 now re-runs itself automatically every time (it's this
|
|
101
|
+
package's own `postinstall`), so a clean clone or CI machine gets it for free. Step
|
|
102
|
+
2 does not and cannot — it's a manual download from Qualcomm, not something this
|
|
103
|
+
package's install can reach out and fetch — so put IT in your own checked-in setup
|
|
104
|
+
script if you want a new teammate's clone or CI machine to get it without a manual
|
|
105
|
+
step. Skip it and a fresh clone fails at CMake configure with a message naming the
|
|
67
106
|
missing piece.
|
|
68
107
|
|
|
69
108
|
**Your app's `android/build.gradle` needs `minSdkVersion = 31`.** A fresh React Native
|
|
@@ -198,6 +237,11 @@ aggregate 10% rate, and the native-error-means-refusal path.
|
|
|
198
237
|
|
|
199
238
|
## Known issues
|
|
200
239
|
|
|
240
|
+
Hitting an actual error (a QNN error code, a manifest merge failure, a decode
|
|
241
|
+
failure) rather than a documented limitation below? See
|
|
242
|
+
[TROUBLESHOOTING.md](TROUBLESHOOTING.md) first — it's real errors from this
|
|
243
|
+
project's own history, most of them more misleading than they look.
|
|
244
|
+
|
|
201
245
|
- **A photo swap's output is capped at 2560 px on the long edge** (and a source photo
|
|
202
246
|
is subsampled to 1920, which costs nothing — it only contributes an identity, never
|
|
203
247
|
output pixels). This is deliberate: the pipeline holds roughly 19 bytes per pixel at
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Real errors hit while building this package, kept here because the QNN/Hexagon
|
|
4
|
+
error codes below are genuinely misleading on their own — the number rarely names
|
|
5
|
+
the actual cause. If you hit one of these while integrating the library into your
|
|
6
|
+
own app, start here before assuming it's your code.
|
|
7
|
+
|
|
8
|
+
For "a face detects on one phone and not another", see [Known issues](README.md#known-issues)
|
|
9
|
+
in the README — that one isn't an error at all, it's documented behaviour.
|
|
10
|
+
|
|
11
|
+
## `Fail to get context blob with err 5000` / `Using newer context binary on old SDK`
|
|
12
|
+
|
|
13
|
+
Your QAIRT SDK (step 2 of [Install](README.md#install)) is older than the one the
|
|
14
|
+
model `.bin` files were compiled against. These files are QNN **context
|
|
15
|
+
binaries** — a graph already compiled and scheduled for one Hexagon architecture,
|
|
16
|
+
not portable weights — and the format they're serialized in
|
|
17
|
+
(`QNN_HTP_CONTEXT_BLOB_VERSION`, a constant in Qualcomm's own `QnnHtpCommon.h`)
|
|
18
|
+
has changed between SDK releases.
|
|
19
|
+
|
|
20
|
+
**Fix:** get a newer QAIRT SDK. This project currently develops against
|
|
21
|
+
**2.49.40.260810** (`QNN_HTP_CONTEXT_BLOB_VERSION 4.0.4`) — treat that as a floor,
|
|
22
|
+
not a ceiling; if a future model update needs a newer blob version than whatever
|
|
23
|
+
SDK you have, you'll see this exact error again.
|
|
24
|
+
|
|
25
|
+
## `err 4000` (`QNN_BACKEND_ERROR_CANNOT_INITIALIZE`)
|
|
26
|
+
|
|
27
|
+
This number on its own tells you almost nothing — it's shown up in this project's
|
|
28
|
+
own history for at least two unrelated root causes (a missing vendor library, and,
|
|
29
|
+
early on, a linking choice that was fixed before this ever became a public API).
|
|
30
|
+
**Don't diagnose from the code. Read `adb logcat` for the actual line immediately
|
|
31
|
+
around it** — in particular, any `dlopen failed:` line naming a specific missing
|
|
32
|
+
library. That line, not the QNN error number, is the real error.
|
|
33
|
+
|
|
34
|
+
## `14001` (`QNN_DEVICE_ERROR_INVALID_CONFIG`)
|
|
35
|
+
|
|
36
|
+
Same advice as `err 4000`, and just as misleading: in this project's own history,
|
|
37
|
+
this exact code showed up from a missing vendor library, and nothing about
|
|
38
|
+
configuration was actually wrong. Read the log line above it.
|
|
39
|
+
|
|
40
|
+
## `dlopen failed: library "libcdsprpc.so" not found: needed by .../libQnnHtpV*Stub.so`
|
|
41
|
+
|
|
42
|
+
The real line behind both codes above, in this project's own case. `libcdsprpc.so`
|
|
43
|
+
is Qualcomm's fastrpc client — the only transport to the Hexagon DSP at all — and
|
|
44
|
+
from `targetSdk` 31+, an app's linker namespace can't see a vendor library unless
|
|
45
|
+
the app's manifest explicitly names it with `<uses-native-library>`.
|
|
46
|
+
|
|
47
|
+
This library's own `AndroidManifest.xml` already declares `libcdsprpc.so` and
|
|
48
|
+
`libadsprpc.so` this way (`required="false"`, so it doesn't make your app
|
|
49
|
+
uninstallable on non-Qualcomm phones), and that merges into your app's manifest
|
|
50
|
+
automatically. **If you see this error anyway,** check your app's *merged*
|
|
51
|
+
manifest — not the one you wrote by hand — for both entries:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
./gradlew :app:processDebugManifest
|
|
55
|
+
# then read app/build/intermediates/merged_manifests/debug/AndroidManifest.xml
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Something in your own manifest, a manifest-merger `tools:` rule, or another
|
|
59
|
+
dependency can suppress a merged-in entry; if it's missing there, that's the bug
|
|
60
|
+
to chase, not this package.
|
|
61
|
+
|
|
62
|
+
## `deviceCreate failed` / "the DSP is not reachable from this process" from `probeDevice()`
|
|
63
|
+
|
|
64
|
+
Your own copy of the QAIRT runtime (step 2 of Install) is missing the Stub/Skel
|
|
65
|
+
pair for *this specific phone's* Hexagon architecture. `libQnnHtp.so` picks that
|
|
66
|
+
pair based on the chip's Hexagon generation, completely independent of which model
|
|
67
|
+
tier got downloaded — so this can happen even with the right models fully
|
|
68
|
+
downloaded and verified. Re-check the copy step: **every** architecture's
|
|
69
|
+
Stub+Skel pair needs to be present (`libQnnHtpV*Stub.so` **and**
|
|
70
|
+
`libQnnHtpV*Skel.so` for each), not a chosen few.
|
|
71
|
+
|
|
72
|
+
## `Could not decode image` for a file `adb shell ls` plainly shows exists
|
|
73
|
+
|
|
74
|
+
Scoped storage, not a bad path. From `targetSdk` 29+, an app with no storage
|
|
75
|
+
permission can't read a file it doesn't own in shared storage (`/sdcard/Download/`
|
|
76
|
+
and similar), even though the file is right there when you look from `adb shell`
|
|
77
|
+
as `shell`, not as your app. Confirm with `run-as <your.app.id> ls <path>` — if
|
|
78
|
+
that comes back empty while a plain `adb shell ls` shows the file, this is it.
|
|
79
|
+
|
|
80
|
+
**Fix:** pass paths your app actually owns —
|
|
81
|
+
`context.getExternalFilesDir(null)` and similar — rather than a raw shared-storage
|
|
82
|
+
path or an unresolved `content://` URI. `swapPhoto`/`swapVideo`/`detectSourceFaces`/
|
|
83
|
+
`detectTargetFaces` all take real filesystem paths, not URIs.
|
|
84
|
+
|
|
85
|
+
## `INSTALL_FAILED_INSUFFICIENT_STORAGE` on a debug install
|
|
86
|
+
|
|
87
|
+
`useLegacyPackaging = true` (required — see [Install](README.md#install)) stores
|
|
88
|
+
the QNN runtime **uncompressed** inside the APK *and* extracts a second real copy
|
|
89
|
+
on disk at install time, so budget for roughly double the runtime's size in actual
|
|
90
|
+
device storage, not just APK size. `adb uninstall` the previous build and
|
|
91
|
+
`pm trim-caches 4G` (or similar) before reinstalling on a tight test device.
|
|
92
|
+
|
|
93
|
+
## Manifest merge fails with `MergeFailureException: Error parsing …` and no line number
|
|
94
|
+
|
|
95
|
+
If you're editing your own `AndroidManifest.xml` near this library's
|
|
96
|
+
`<uses-native-library>` entries and hit this with no useful location: check for a
|
|
97
|
+
literal `--` inside an XML comment. It's illegal in XML, the manifest merger's
|
|
98
|
+
error gives no line number for it, and it's an easy thing to type by accident in a
|
|
99
|
+
comment describing what a permission is *for*.
|
|
@@ -10,6 +10,7 @@ import java.io.File
|
|
|
10
10
|
import java.util.concurrent.Callable
|
|
11
11
|
import java.util.concurrent.ExecutorService
|
|
12
12
|
import java.util.concurrent.Executors
|
|
13
|
+
import java.util.concurrent.TimeUnit
|
|
13
14
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
14
15
|
|
|
15
16
|
class FacefusionModule(reactContext: ReactApplicationContext) :
|
|
@@ -170,8 +171,15 @@ class FacefusionModule(reactContext: ReactApplicationContext) :
|
|
|
170
171
|
return
|
|
171
172
|
}
|
|
172
173
|
videoWorker.execute {
|
|
173
|
-
VideoSwapService.start(reactApplicationContext)
|
|
174
174
|
try {
|
|
175
|
+
// Must be inside the try: on minSdk 31+, starting a foreground service from
|
|
176
|
+
// outside a foreground/allowed context throws
|
|
177
|
+
// ForegroundServiceStartNotAllowedException. Outside this try that exception had
|
|
178
|
+
// nowhere to go but off this Runnable entirely -- `promise` never settled (the JS
|
|
179
|
+
// call hung forever) and the `finally` below never ran, so `videoBusy` stayed
|
|
180
|
+
// `true` and every later swapVideo() call rejected E_BUSY for the rest of the
|
|
181
|
+
// process's life.
|
|
182
|
+
VideoSwapService.start(reactApplicationContext)
|
|
175
183
|
val result = VideoSwap.run(
|
|
176
184
|
reactApplicationContext, sourcePath, targetPath, outputPath, swapConfig(options),
|
|
177
185
|
sourceFaceBox(options), targetFaceBox(options), targetFps(options),
|
|
@@ -232,13 +240,28 @@ class FacefusionModule(reactContext: ReactApplicationContext) :
|
|
|
232
240
|
}
|
|
233
241
|
|
|
234
242
|
override fun invalidate() {
|
|
243
|
+
// Cooperative cancellation first, so anything already running on `worker`/`videoWorker`
|
|
244
|
+
// gets to the next `throwIfCancelled()` check and exits quickly instead of running its
|
|
245
|
+
// full course.
|
|
235
246
|
ModelDownload.cancel()
|
|
236
247
|
VideoSwap.cancel()
|
|
237
|
-
|
|
248
|
+
|
|
249
|
+
// `shutdown()` alone only stops NEW submissions -- a processFrame()/initEx() call
|
|
250
|
+
// already in flight keeps running on its own thread afterwards. `NativePipe.release()`
|
|
251
|
+
// used to run right after `shutdown()` with nothing in between, so a job that was
|
|
252
|
+
// mid-call when the module got invalidated (a JS reload while a swap is running, say)
|
|
253
|
+
// could still be touching `g_pipe` in native code the instant it was freed -- a
|
|
254
|
+
// use-after-free, which shows up as a native crash, not a catchable Kotlin exception.
|
|
255
|
+
// Waiting for real termination first closes that gap; the two executors that never
|
|
256
|
+
// touch `g_pipe` (`downloader`, `gallery`) don't need the same wait.
|
|
238
257
|
worker.shutdown()
|
|
239
|
-
downloader.shutdown()
|
|
240
258
|
videoWorker.shutdown()
|
|
259
|
+
downloader.shutdown()
|
|
241
260
|
gallery.shutdown()
|
|
261
|
+
worker.awaitTermination(10, TimeUnit.SECONDS)
|
|
262
|
+
videoWorker.awaitTermination(10, TimeUnit.SECONDS)
|
|
263
|
+
|
|
264
|
+
NativePipe.release()
|
|
242
265
|
super.invalidate()
|
|
243
266
|
}
|
|
244
267
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-facefusion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
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
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"src",
|
|
17
17
|
"lib",
|
|
18
18
|
"android",
|
|
19
|
+
"TROUBLESHOOTING.md",
|
|
19
20
|
"scripts/fetch-upstream.sh",
|
|
20
21
|
"scripts/check-tarball.sh",
|
|
21
22
|
"patches",
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"example": "npm run --workspace=example",
|
|
38
39
|
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
39
40
|
"prepare": "bob build",
|
|
41
|
+
"postinstall": "./scripts/fetch-upstream.sh || echo 'react-native-facefusion: could not auto-fetch the upstream C++ engine (see the error above). Run ./node_modules/react-native-facefusion/scripts/fetch-upstream.sh by hand before building Android -- most likely cause is no network/git at install time.'",
|
|
40
42
|
"typecheck": "tsc",
|
|
41
43
|
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
42
44
|
"prepublishOnly": "./scripts/check-tarball.sh"
|