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,86 @@
1
+ package com.facefusion
2
+
3
+ import com.facefusion.mobile.NativePipe
4
+
5
+ /**
6
+ * Upstream's NSFW content gate (ADR-0003), run on the public API path where a JS caller
7
+ * cannot skip it -- no `skipCheck` option exists anywhere above [PhotoSwap]/[VideoSwap].
8
+ *
9
+ * **This is the one file a personal, gate-free build deletes.** `android/build.gradle`
10
+ * generates a no-op stand-in with this class's public surface when the file is absent, so
11
+ * deleting it is the only change needed -- [PhotoSwap]/[VideoSwap] call [ContentGate]
12
+ * exactly the same way either build compiles it. That build renames itself `-dev` with a
13
+ * distinct `applicationId` (see `example/android/app/build.gradle`), upstream's own switch,
14
+ * not a bypass flag.
15
+ *
16
+ * Gates on `nsfw_2` alone, not upstream's 2-of-3 vote across three models -- 461 MB was not
17
+ * worth it against a 266 MB app (`docs/roadmap.md` 2) -- and the quantised build every tier
18
+ * but v79 uses sits ~0.087 mean closer to flagging. Both limits belong in the README, not
19
+ * hidden, per ADR-0003.
20
+ */
21
+ object ContentGate {
22
+
23
+ /** Refused. [isQuantised] lets a caller mention the known quantisation bias in its own
24
+ * message without this class importing any UI concern. */
25
+ class Refused(message: String, val isQuantised: Boolean) : Exception(message)
26
+
27
+ // content_analyser.py:detect_with_nsfw_2 -- logit[0] - logit[1] > 0.25 flags a frame.
28
+ private const val THRESHOLD = 0.25f
29
+
30
+ // Refuse a video only on the AGGREGATE rate across its sampled frames, not on the first
31
+ // flagged one -- a single false positive should not sink an otherwise-clean clip.
32
+ private const val VIDEO_FLAG_RATE = 0.10
33
+
34
+ /**
35
+ * One frame -- a still photo. Call with the TARGET frame, the content actually being
36
+ * processed, before [NativePipe.processFrame] runs: refusing up front skips the swap
37
+ * entirely instead of doing the work and then discarding it.
38
+ */
39
+ fun checkFrame(bgr: ByteArray, w: Int, h: Int) {
40
+ val quantised = NativePipe.contentGateIsQuantised()
41
+ val score = NativePipe.contentScore(bgr, w, h)
42
+ // A failure to measure is treated as an error, never as "allow" (docs/02-upstream.md).
43
+ if (score.isNaN()) {
44
+ throw Refused("Content check failed: ${NativePipe.lastError()}", quantised)
45
+ }
46
+ if (score > THRESHOLD) {
47
+ throw Refused("Content check flagged this image (score %.2f)".format(score), quantised)
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Accumulates one sampled frame per second of a video target. [VideoSwap] already decodes
53
+ * every frame for the swap itself and buffers every encoded frame until the whole clip is
54
+ * done before muxing -- so [sample] rides that same loop for free, and [finish] applies the
55
+ * rate rule before the muxer (and thus [outputPath]) is ever touched, exactly like refusing
56
+ * a still up front.
57
+ */
58
+ class VideoSampler {
59
+ private var sampled = 0
60
+ private var flagged = 0
61
+ private var quantised = false
62
+
63
+ fun sample(bgr: ByteArray, w: Int, h: Int) {
64
+ quantised = NativePipe.contentGateIsQuantised()
65
+ val score = NativePipe.contentScore(bgr, w, h)
66
+ if (score.isNaN()) {
67
+ throw Refused("Content check failed: ${NativePipe.lastError()}", quantised)
68
+ }
69
+ sampled++
70
+ if (score > THRESHOLD) flagged++
71
+ }
72
+
73
+ /** Call once, after every frame has been offered to [sample]. A clip too short to have
74
+ * sampled anything is not refused -- there is nothing measured to refuse it on. */
75
+ fun finish() {
76
+ if (sampled == 0) return
77
+ val rate = flagged.toDouble() / sampled
78
+ if (rate > VIDEO_FLAG_RATE) {
79
+ throw Refused(
80
+ "Content check flagged ${(rate * 100).toInt()}% of sampled frames (limit 10%)",
81
+ quantised,
82
+ )
83
+ }
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,158 @@
1
+ package com.facefusion
2
+
3
+ import android.content.Context
4
+ import com.facefusion.mobile.NativePipe
5
+
6
+ /**
7
+ * What this phone's Hexagon NPU says about itself.
8
+ *
9
+ * [ok] is the only field that decides whether the rest mean anything. `false` means the
10
+ * probe failed — no QNN runtime, no Hexagon DSP, an emulator — and **not** that the chip
11
+ * is old or unsupported. [tier] and [tierChain] are still filled in either way, because
12
+ * the native layer's fallback (`v68`, the floor that runs on every HTP) is a usable
13
+ * answer even when nothing could be measured.
14
+ */
15
+ data class DeviceProbe(
16
+ val ok: Boolean,
17
+ val tier: String,
18
+ val tierChain: List<String>,
19
+ val arch: Int,
20
+ val vtcmMb: Int,
21
+ val socModel: Int,
22
+ val signedPd: Boolean,
23
+ val dlbc: Boolean,
24
+ val error: String,
25
+ ) {
26
+ companion object {
27
+ /** The tier every unmeasured device falls back to; mirrors `tierChain()` in ffqnn.cpp. */
28
+ private const val FALLBACK_TIER = "v68"
29
+
30
+ /**
31
+ * Cached because the chain is a property of the *silicon* and cannot change while the
32
+ * process lives, and because measuring it brings the QNN backend up — far too expensive
33
+ * to repeat every time a screen wants to know which models to download.
34
+ *
35
+ * Only a real answer is cached. A probe that failed must not be remembered as "this chip
36
+ * can load nothing" for the rest of the process, and the fallback is not a measurement.
37
+ *
38
+ * The chain is cached; the tier [ModelPaths.tier] resolves against disk deliberately is
39
+ * not. What a device *can* load is fixed; what is *on disk* changes the moment a
40
+ * download finishes.
41
+ */
42
+ @Volatile
43
+ private var chainCache: List<String>? = null
44
+
45
+ /**
46
+ * Every tier this chip can load, best first — `["v81", "v73", "v68"]`.
47
+ *
48
+ * **Not** "this tier and every older one": the native `tierChain()` skips arches the
49
+ * chip cannot run, so it must never be reconstructed from a single tier.
50
+ *
51
+ * Falls back to a one-entry `v68` chain when nothing could be measured, which is a
52
+ * usable answer rather than an error — see the class docs.
53
+ */
54
+ fun tierChain(context: Context): List<String> {
55
+ chainCache?.let { return it }
56
+ if (NativePipe.loadError != null) return listOf(FALLBACK_TIER)
57
+
58
+ val libDir = context.applicationInfo.nativeLibraryDir
59
+ val chain = NativePipe.probeTierChain(libDir, libDir)
60
+ .split(',')
61
+ .map { it.trim() }
62
+ .filter { it.isNotEmpty() }
63
+
64
+ if (chain.isEmpty()) return listOf(FALLBACK_TIER)
65
+ chainCache = chain
66
+ return chain
67
+ }
68
+
69
+ /**
70
+ * Runs the probe. **Blocking** — it `dlopen`s the QNN backend and creates a device
71
+ * handle, which is tens of milliseconds on real hardware and a failed `dlopen`
72
+ * anywhere else. Call it off the main thread.
73
+ */
74
+ fun probe(context: Context): DeviceProbe {
75
+ NativePipe.loadError?.let { return failed("libffnative.so did not load: $it") }
76
+
77
+ val chain = tierChain(context)
78
+
79
+ // Both directories are the same one. The QAIRT runtime ships as ordinary .so files
80
+ // in jniLibs, so the installer extracts backend, stub and skel side by side into
81
+ // the app's nativeLibraryDir -- and that extraction is exactly what
82
+ // `useLegacyPackaging = true` guarantees, because the backend is opened by path.
83
+ val libDir = context.applicationInfo.nativeLibraryDir
84
+ val info = parse(NativePipe.probeDeviceInfo(libDir, libDir))
85
+ val ok = info["ok"] == "1"
86
+
87
+ return DeviceProbe(
88
+ ok = ok,
89
+ tier = chain.first(),
90
+ tierChain = chain,
91
+ arch = info["arch"]?.toIntOrNull() ?: 0,
92
+ vtcmMb = info["vtcm"]?.toIntOrNull() ?: 0,
93
+ socModel = info["soc"]?.toIntOrNull() ?: 0,
94
+ signedPd = info["signedPd"] == "1",
95
+ dlbc = info["dlbc"] == "1",
96
+ // lastError() is whatever failed most recently, so it is only meaningful when
97
+ // something did. Reading it on the success path would report a stale message.
98
+ error = if (ok) "" else explain(NativePipe.lastError(), libDir),
99
+ )
100
+ }
101
+
102
+ /**
103
+ * Turns the native layer's `deviceCreate failed -- the DSP is not reachable from this
104
+ * process` into something a person can act on.
105
+ *
106
+ * That message is accurate and useless: the overwhelmingly common cause is not a broken
107
+ * phone but a **missing Hexagon skel for this chip's architecture**. `libQnnHtp.so`
108
+ * loads `libQnnHtpV<arch>Skel.so` chosen from the *silicon*, not from the model tier --
109
+ * so a build that bundles v73/v79/v81 cannot reach the DSP on an 8 Gen 1 (v69) no
110
+ * matter which models are on disk. Reported 2026-09-12 by the first person outside
111
+ * Saad's own 8 Elite to run this.
112
+ *
113
+ * The arch cannot be named here: it is measured by the very call that just failed, so
114
+ * on this path it reads 0. What *can* be named is what this build actually ships, which
115
+ * is enough for anyone to see the mismatch.
116
+ */
117
+ private fun explain(nativeError: String, libDir: String): String {
118
+ if (!nativeError.contains("deviceCreate")) return nativeError
119
+ val bundled = java.io.File(libDir)
120
+ .list { _, name -> SKEL.matches(name) }
121
+ ?.map { SKEL.find(it)!!.groupValues[1] }
122
+ ?.sortedBy { it.toIntOrNull() ?: 0 }
123
+ .orEmpty()
124
+ return when {
125
+ bundled.isEmpty() ->
126
+ "$nativeError. No Hexagon runtime is bundled in this build at all -- see the " +
127
+ "QAIRT SDK step in the README."
128
+ else ->
129
+ "$nativeError. This build bundles the Hexagon runtime for " +
130
+ bundled.joinToString(", ") { "v$it" } +
131
+ " only; this chip needs a different one, so no swap can run here."
132
+ }
133
+ }
134
+
135
+ private val SKEL = Regex("""libQnnHtpV(\d+)Skel\.so""")
136
+
137
+ private fun failed(reason: String) = DeviceProbe(
138
+ ok = false,
139
+ tier = FALLBACK_TIER,
140
+ tierChain = listOf(FALLBACK_TIER),
141
+ arch = 0,
142
+ vtcmMb = 0,
143
+ socModel = 0,
144
+ signedPd = false,
145
+ dlbc = false,
146
+ error = reason,
147
+ )
148
+
149
+ /** `ok=1;arch=79;vtcm=8;…` -> a map. Pairs without an `=` are dropped. */
150
+ private fun parse(info: String): Map<String, String> =
151
+ info.split(';')
152
+ .mapNotNull { field ->
153
+ val i = field.indexOf('=')
154
+ if (i <= 0) null else field.substring(0, i) to field.substring(i + 1)
155
+ }
156
+ .toMap()
157
+ }
158
+ }
@@ -0,0 +1,57 @@
1
+ package com.facefusion
2
+
3
+ /**
4
+ * Crops a packed BGR frame to one face's box, and pastes a same-sized crop back — how
5
+ * [PhotoSwap]/[VideoSwap] swap only a chosen *target* face without a new native entry
6
+ * point: [com.facefusion.mobile.NativePipe.processFrame] already swaps every face it's
7
+ * handed, so handing it a tight crop around one face swaps only that one. The same trick
8
+ * [SourceFaces] already uses on the source side for `setSource` — this is the BGR-buffer
9
+ * version of it, since [PhotoSwap]/[VideoSwap] both work in packed BGR, not `Bitmap`s, by
10
+ * the time a target face box would apply.
11
+ */
12
+ object FaceCrop {
13
+
14
+ /** Same margin [SourceFaces] uses on the source side, for the same reason: the
15
+ * detector/landmarker inside `processFrame` need a bit of surrounding context, not a
16
+ * box drawn exactly to the model's own edges. */
17
+ const val MARGIN = 0.4f
18
+
19
+ /** [box] (`[left, top, right, bottom]`, as returned by [TargetFaces.detect]) expanded by
20
+ * [margin] and clamped to `0..w`/`0..h`, as pixel ints. */
21
+ fun rect(box: FloatArray, w: Int, h: Int, margin: Float = MARGIN): IntArray {
22
+ require(box.size == 4) { "box must be [left, top, right, bottom]" }
23
+ val (boxLeft, boxTop, boxRight, boxBottom) = box
24
+ val bw = boxRight - boxLeft
25
+ val bh = boxBottom - boxTop
26
+ val left = (boxLeft - bw * margin).toInt().coerceIn(0, w - 1)
27
+ val top = (boxTop - bh * margin).toInt().coerceIn(0, h - 1)
28
+ val right = (boxRight + bw * margin).toInt().coerceIn(left + 1, w)
29
+ val bottom = (boxBottom + bh * margin).toInt().coerceIn(top + 1, h)
30
+ return intArrayOf(left, top, right, bottom)
31
+ }
32
+
33
+ /** Extracts `rect` from a packed BGR buffer (3 bytes/pixel, no row padding) of width [w]. */
34
+ fun crop(bgr: ByteArray, w: Int, rect: IntArray): ByteArray {
35
+ val left = rect[0]
36
+ val top = rect[1]
37
+ val cw = rect[2] - left
38
+ val ch = rect[3] - top
39
+ val out = ByteArray(cw * ch * 3)
40
+ for (row in 0 until ch) {
41
+ System.arraycopy(bgr, ((top + row) * w + left) * 3, out, row * cw * 3, cw * 3)
42
+ }
43
+ return out
44
+ }
45
+
46
+ /** Writes a buffer from [crop] back into [bgr] (width [w]) at [rect]. The inverse of
47
+ * [crop] — call with the same `rect` used to produce it. */
48
+ fun paste(bgr: ByteArray, w: Int, cropped: ByteArray, rect: IntArray) {
49
+ val left = rect[0]
50
+ val top = rect[1]
51
+ val cw = rect[2] - left
52
+ val ch = rect[3] - top
53
+ for (row in 0 until ch) {
54
+ System.arraycopy(cropped, row * cw * 3, bgr, ((top + row) * w + left) * 3, cw * 3)
55
+ }
56
+ }
57
+ }