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
+ [![npm version](https://img.shields.io/npm/v/react-native-facefusion.svg)](https://www.npmjs.com/package/react-native-facefusion)
4
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+ [![platform: android](https://img.shields.io/badge/platform-android-3ddc84.svg)](#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
- Three steps, not one — a plain `npm install` is not enough on its own.
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. Fetch the upstream C++ engine this module wraps. Not committed to this repo or
35
- # to the npm package see "Why the extra steps" below.
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. Get Qualcomm's QAIRT SDK (Community edition a plain ZIP, no account needed):
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
- **Script steps 1 and 2.** Both write into `node_modules/`, so a fresh `npm install`
64
- or `npm ci` wipes them. Put them in a checked-in setup script (or a `postinstall`)
65
- rather than running them by hand once and forgetting a CI machine or a new
66
- teammate's clone will otherwise fail at CMake configure with a message naming the
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
- NativePipe.release()
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.0",
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"