├── .gitignore
├── LICENSE
├── README.md
├── _media
├── example.gif
├── face_scan_icon.png
└── logo.png
├── logo.png
├── requirements.txt
└── userscript
└── visage.user.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # visage stuff we want to ignore
94 | matcher/performers.json
95 | matcher/face.db
96 | matcher/face.json
97 | hasher/weights/*.h5
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 cc1234475
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Deprecated
2 |
3 | This project has been deprecated favor of a plugin.
4 |
5 | You can find it here:
6 |
7 | https://github.com/stashapp/CommunityScripts/tree/main/plugins/visage
8 |
9 | # Visage
10 |
11 | 
12 |
13 | Visage is a userscript to do facial recognition on video's and images in [Stash](https://github.com/stashapp/stash).
14 |
15 | Once installed, A new icon will show on a scene's page next to the organized button.
16 |
17 | 
18 |
19 | # How do I use it?
20 |
21 | 
22 |
23 | # Create your own database.
24 |
25 | Please check out https://github.com/cc1234475/visage-ml
26 |
--------------------------------------------------------------------------------
/_media/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc1234475/visage/2749b4c0da26ee7c518081778f986ad53dd15a05/_media/example.gif
--------------------------------------------------------------------------------
/_media/face_scan_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc1234475/visage/2749b4c0da26ee7c518081778f986ad53dd15a05/_media/face_scan_icon.png
--------------------------------------------------------------------------------
/_media/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc1234475/visage/2749b4c0da26ee7c518081778f986ad53dd15a05/_media/logo.png
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc1234475/visage/2749b4c0da26ee7c518081778f986ad53dd15a05/logo.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi==0.85.0
2 | python-multipart==0.0.5
3 | pydantic==1.10.2
4 | annoy==1.17.1
5 | deepface==0.0.75
6 | uvicorn[standard]==0.18.3
--------------------------------------------------------------------------------
/userscript/visage.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name visage
3 | // @namespace https://github.com/cc1234475
4 | // @version 0.5.3
5 | // @description Match faces to performers
6 | // @author cc12344567
7 | // @match http://localhost:9999/*
8 | // @connect localhost
9 | // @connect hf.space
10 | // @grant GM_xmlhttpRequest
11 | // @grant unsafeWindow
12 | // @require https://code.jquery.com/jquery-2.0.3.min.js
13 | // @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
14 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js
15 | // ==/UserScript==
16 |
17 | var VISAGE_API_URL = "https://cc1234-stashface.hf.space/api/predict";
18 | // var VISAGE_API_URL = "http://localhost:7860/api/predict";
19 | var THRESHOLD = 20.0; // remove matches with a distance higher than this
20 | var MAX_RESULTS = 3; // number of results to show, don't change this for now
21 |
22 | (function () {
23 | "use strict";
24 |
25 | const {
26 | stash,
27 | Stash,
28 | waitForElementId,
29 | waitForElementClass,
30 | waitForElementByXpath,
31 | getElementByXpath,
32 | insertAfter,
33 | createElementFromHTML,
34 | } = unsafeWindow.stash;
35 |
36 | function waitForElm(selector) {
37 | return new Promise((resolve) => {
38 | if (document.querySelector(selector)) {
39 | return resolve(document.querySelector(selector));
40 | }
41 |
42 | const observer = new MutationObserver((mutations) => {
43 | if (document.querySelector(selector)) {
44 | resolve(document.querySelector(selector));
45 | observer.disconnect();
46 | }
47 | });
48 |
49 | observer.observe(document.body, {
50 | childList: true,
51 | subtree: true,
52 | });
53 | });
54 | }
55 |
56 | var scanning = `
57 |
58 |
59 |
60 |
61 |
62 |
Scanning image for face
63 |
64 |
65 |
70 |
71 |
72 |
`;
73 |
74 | var top = `
75 |
76 |
77 |
78 |
79 |
`;
80 |
81 | var match = (id, name, image, confidence) => `
82 |
`;
95 |
96 | var bottom = `
97 |
98 |
104 |
105 |
106 |
`;
107 |
108 | async function add_performer(id_, name) {
109 | // find a performer with the same stash id in the user instance of stash
110 | var performers = await get_performers(id_);
111 | // if the users doesn't have a performer with the same stash id, get the data from stash box and create a new performer
112 | if (performers.length === 0) {
113 | var performer = await get_performer_data_based_on_name(id_);
114 |
115 | if (performer === undefined) {
116 | alert("Could not retrieve performer data from stash box");
117 | return;
118 | }
119 |
120 | performer.image = performer.images[0];
121 | var endpoint = await get_stashbox_endpoint();
122 |
123 | // delete some fields that are not needed and will not be accepted by local stash instance
124 | delete performer.images;
125 | delete performer.remote_site_id;
126 |
127 | performer.stash_ids = [{ endpoint: endpoint, stash_id: id_ }];
128 |
129 | id_ = await create_performer(performer);
130 | id_ = id_.data.performerCreate.id;
131 | } else {
132 | id_ = performers[0].id;
133 | }
134 |
135 | let [scenario, scenario_id] = get_scenario_and_id();
136 | var perform_ids;
137 |
138 | if (scenario === "scenes") {
139 | perform_ids = await get_performers_for_scene(scenario_id);
140 |
141 | if (perform_ids.includes(id_)) {
142 | alert("Performer already assigned to scene");
143 | return;
144 | }
145 |
146 | perform_ids.push(id_);
147 |
148 | await update_scene(scenario_id, perform_ids);
149 | } else if (scenario === "images") {
150 | perform_ids = await get_performers_for_image(scenario_id);
151 |
152 | if (perform_ids.includes(id_)) {
153 | alert("Performer already assigned to scene");
154 | return;
155 | }
156 |
157 | perform_ids.push(id_);
158 |
159 | await update_image(scenario_id, perform_ids);
160 | }
161 |
162 | location.reload();
163 | }
164 |
165 | function get_scenario_and_id() {
166 | var result = document.URL.match(/(scenes|images)\/(\d+)/);
167 | var scenario = result[1];
168 | var scenario_id = result[2];
169 | return [scenario, scenario_id];
170 | }
171 |
172 | async function get_performers(performer_id) {
173 | const reqData = {
174 | query: `{
175 | findPerformers( performer_filter: {stash_id: {value: "${performer_id}", modifier: EQUALS}}){
176 | performers {
177 | name
178 | id
179 | }
180 | }
181 | }`,
182 | };
183 | var results = await stash.callGQL(reqData);
184 | return results.data.findPerformers.performers;
185 | }
186 |
187 | async function get_performers_for_scene(scene_id) {
188 | const reqData = {
189 | query: `{
190 | findScene(id: "${scene_id}") {
191 | performers {
192 | id
193 | }
194 | }
195 | }`,
196 | };
197 | var result = await stash.callGQL(reqData);
198 | return result.data.findScene.performers.map((p) => p.id);
199 | }
200 |
201 | async function get_performers_for_image(image_id) {
202 | const reqData = {
203 | query: `{
204 | findImage(id: "${image_id}") {
205 | performers {
206 | id
207 | }
208 | }
209 | }`,
210 | };
211 | var result = await stash.callGQL(reqData);
212 | return result.data.findImage.performers.map((p) => p.id);
213 | }
214 |
215 | async function update_scene(scene_id, performer_ids) {
216 | const reqData = {
217 | variables: { input: { id: scene_id, performer_ids: performer_ids } },
218 | query: `mutation sceneUpdate($input: SceneUpdateInput!){
219 | sceneUpdate(input: $input) {
220 | id
221 | }
222 | }`,
223 | };
224 | return stash.callGQL(reqData);
225 | }
226 |
227 | async function update_image(image_id, performer_ids) {
228 | const reqData = {
229 | variables: { input: { id: image_id, performer_ids: performer_ids } },
230 | query: `mutation imageUpdate($input: ImageUpdateInput!){
231 | imageUpdate(input: $input) {
232 | id
233 | }
234 | }`,
235 | };
236 | return stash.callGQL(reqData);
237 | }
238 |
239 | async function get_stashbox_endpoint() {
240 | const reqData = {
241 | query: `{
242 | configuration {
243 | general {
244 | stashBoxes {
245 | endpoint
246 | }
247 | }
248 | }
249 | }`,
250 | };
251 | var result = await stash.callGQL(reqData);
252 | return result.data.configuration.general.stashBoxes[0].endpoint;
253 | }
254 |
255 | async function get_performer_data_based_on_name(stash_id) {
256 | const reqData = {
257 | variables: {
258 | source: {
259 | stash_box_index: 0,
260 | },
261 | input: {
262 | query: stash_id,
263 | },
264 | },
265 | query: `query ScrapeSinglePerformer($source: ScraperSourceInput!, $input: ScrapeSinglePerformerInput!) {
266 | scrapeSinglePerformer(source: $source, input: $input) {
267 | name
268 | gender
269 | url
270 | twitter
271 | instagram
272 | birthdate
273 | ethnicity
274 | country
275 | eye_color
276 | height
277 | measurements
278 | fake_tits
279 | career_length
280 | tattoos
281 | piercings
282 | aliases
283 | images
284 | details
285 | death_date
286 | hair_color
287 | weight
288 | remote_site_id
289 | }
290 | }`,
291 | };
292 | var result = await stash.callGQL(reqData);
293 | return result.data.scrapeSinglePerformer.filter(
294 | (p) => p.remote_site_id === stash_id
295 | )[0];
296 | }
297 |
298 | async function create_performer(performer) {
299 | const reqData = {
300 | variables: { input: performer },
301 | query: `mutation performerCreate($input: PerformerCreateInput!) {
302 | performerCreate(input: $input){
303 | id
304 | }
305 | }`,
306 | };
307 | return stash.callGQL(reqData);
308 | }
309 |
310 | function show_matches(matches) {
311 | var html = top;
312 | console.log(matches.length);
313 | for (var i = 0; i < matches.length; i++) {
314 | let per = matches[i];
315 | html += match(i, per.name, per.image, round(per.confidence));
316 | }
317 | html += bottom;
318 | $("body").append(html);
319 |
320 | $("#face_cancel").click(function () {
321 | close_modal();
322 | });
323 |
324 | $("#face_toggle").click(function () {
325 | var obj = $(".ModalComponent");
326 | if (obj.css("opacity") == "0.1") {
327 | $(".ModalComponent").css("opacity", "1.0");
328 | } else {
329 | $(".ModalComponent").css("opacity", "0.1");
330 | }
331 | });
332 |
333 | $("#face-0").click(function () {
334 | add_performer(matches[0].id, matches[0].name);
335 | close_modal();
336 | });
337 |
338 | $("#face-1").click(function () {
339 | add_performer(matches[1].id, matches[1].name);
340 | close_modal();
341 | });
342 |
343 | $("#face-2").click(function () {
344 | add_performer(matches[2].id, matches[2].name);
345 | close_modal();
346 | });
347 | }
348 |
349 | function recognize() {
350 | let [scenario, scenario_id] = get_scenario_and_id();
351 | var selector;
352 | if (scenario === "scenes") {
353 | selector = "#VideoJsPlayer";
354 | } else if (scenario === "images") {
355 | selector = ".image-image";
356 | }
357 |
358 | html2canvas(document.querySelector(selector)).then((canvas) => {
359 | let image = canvas.toDataURL("image/jpg");
360 | $("body").append(scanning);
361 | var data = {"data": [image, THRESHOLD, MAX_RESULTS]};
362 | var requestDetails = {
363 | method: "POST",
364 | url: VISAGE_API_URL,
365 | data: JSON.stringify(data),
366 | headers: {
367 | "Content-Type": "application/json; charset=utf-8"
368 | },
369 | onload: function (response) {
370 | var data = JSON.parse(response.responseText);
371 | close_modal();
372 | if (data.data[0].length === 0) {
373 | alert("No matches found");
374 | return;
375 | }
376 | show_matches(data.data[0]);
377 | },
378 | onerror: function (response) {
379 | close_modal();
380 | alert("Error: " + response.responseText);
381 | },
382 | };
383 | GM_xmlhttpRequest(requestDetails);
384 | });
385 | }
386 |
387 | function close_modal() {
388 | $(".ModalComponent").remove();
389 | }
390 |
391 | function round(value) {
392 | return +parseFloat(value).toFixed(2);
393 | }
394 |
395 | function create_button(action) {
396 | waitForElm(".ml-auto .btn-group").then(() => {
397 | const grp = document.querySelector(".ml-auto .btn-group");
398 | const btn = document.createElement("button");
399 | btn.setAttribute("id", "facescan");
400 | btn.setAttribute("title", "Scan for performer");
401 | btn.classList.add("btn", "btn-secondary", "minimal");
402 | btn.innerHTML =
403 | '';
404 | btn.onclick = action;
405 | grp.appendChild(btn);
406 | });
407 | }
408 |
409 | stash.addEventListener("page:scene", function () {
410 | create_button(recognize);
411 | });
412 |
413 | stash.addEventListener("page:image", function () {
414 | create_button(recognize);
415 | });
416 | })();
417 |
--------------------------------------------------------------------------------