Resource icon

Other Appearance Preset Extractor

cartitolaire

Member
Messages
10
Reactions
35
Points
13
cartitolaire submitted a new resource:

Appearance Preset Extractor - Automatically export presets from existing scenes

This tool automatically export presets from existing scenes and collects them in an "extracted" folder under presets. The presets will use the thumbnail for the source scene.

The behavior is comparable to ZFHX's "Var Looks Scanner plugin", although the implementation is entirely different.

This is a standalone python script, not an in-program plugin. This requires python3 to be installed and working already.

Usage Instructions:

  1. - Save the file to your root directory -- the...

Read more about this resource...
 
Appearance presets include clothing, but an option to export as clothing-only (without morphs, etc) is a good idea.
 
would you have a step by step guide for python newbies?
As a newbie, I was able to paste the code into IDLE or a text file as a .py file and dropped it in the VaM directory, but getting it to execute using your examples and changing the proper variables such as the folder or file names is stopping me. I'm sure I have tqdm installed as well so maybe it has something to do with that. Maybe OP or another commentor will make a more detailed guide as ZFHX's method doesn't work well for us who have thousands of Looks to save.
 
As a newbie, I was able to paste the code into IDLE or a text file as a .py file and dropped it in the VaM directory, but getting it to execute using your examples and changing the proper variables such as the folder or file names is stopping me. I'm sure I have tqdm installed as well so maybe it has something to do with that. Maybe OP or another commentor will make a more detailed guide as ZFHX's method doesn't work well for us who have thousands of Looks to save.
1. be sure to have pthon installed
2. copy the downloaded code and paste it in a blank file in your root VAM folder, and name it "extract_appearance.py"
3. open a powershell and type:
cd "your_VAM_directory"
py .\ extract_appearance.py "AddonPackages/*.var

you'll find a folder named "extracted" in custom/atom/person/apparences withn the looks of the scenes in addonpackages.
hope this helps
 
1. be sure to have pthon installed
2. copy the downloaded code and paste it in a blank file in your root VAM folder, and name it "extract_appearance.py"
3. open a powershell and type:
cd "your_VAM_directory"
py .\ extract_appearance.py "AddonPackages/*.var

you'll find a folder named "extracted" in custom/atom/person/apparences withn the looks of the scenes in addonpackages.
hope this helps
Yes, this worked for me. Thank you! I also forgot my VaM is on my external nvme drive so I needed to route the directory correctly.
 
Last year I've created a PowerShell script that extracts all scenes/looks/presets from VARS in a folder to the specified directories, scans all VAP and JSON files and replaces 'SELF:' occurrencies with a proper name of the dependency VAR name (to make it independent from local VAM installation and to always call for a proper VAR), and then, whenever I need certain dependencies present in AddonPackages folder, I use FeelFar's VarManager option 'SAVES JSONFile' that scans all scenes/looks/presets in VAM folders and creates symbolic links to dependencies from a VAR repository. Very fast and convenient tool, that made my VAM installations not needing so much drive space.

Your script works fine with my prepared scenes (with removed SELF: local calls for hairs, clothes, morphs, etc), but does not work with original VARS and scenes extracted from those VARS.

Fist I extract scenes from VAR files with this Windows CMD script:

Code:
SET source=K:\VARS_2_EXTRACT
SET DEST=H:\VAM\EXTRACTED\SCENES

FOR /F "TOKENS=*" %%F IN ('DIR /S /B "%source%\*.var"') DO "C:\Program Files\7-Zip\7z.exe" e "%%~fF" -aos -r -o"%DEST%\*" Saves\scene\*.jpg Saves\scene\*.json

It creates subdirectories in a proper format for the following script to digest. Here's my PowerShell script that replaces SELF: to VAR name in all VAP and JSON files. May be you can convert it to Python.

Code:
Get-ChildItem $PSScriptRoot -Recurse -Include "*.json","*.vap" | ForEach-Object {
write-host $_.BaseName
    (Get-Content -Raw -LiteralPath $($_.FullName)) -replace 'SELF:', ($_.Directory.Name + ':') | Set-Content -LiteralPath $_.FullName
}

Now your script will work just fine with the output files, because they have proper dependencies listed inside them. Of course you have to put all those VARS inside AddonPackages folder.
 
I was able to use the given PowerShell script to modify the Python script to replace SELF references with the actual dependency. Per

Here's my version of the script that replaces SELF references with the actual var file:

Python:
#!/bin/python3

SCRIPT_VERSION = '1.1'

import json
import sys
import os
import zipfile
import re
import glob
import traceback
import logging

try:
    from tqdm import tqdm as tqdmfunc
except ImportError:
    print("tqdm not installed, not showing progress bar")
    def tqdmfunc(iterator):
        yield from iterator

out_dir = None # Set in main()

dupes = set()

# logging.getLogger().setLevel(logging.INFO)

def getCleanedPersonAtoms(scenejson):
    for atom in scenejson['atoms']:
        if atom['type'] != "Person":
            continue
        storables = atom['storables']
        for storable in [*storables]:
            storable.pop("position", None)
            storable.pop("rotation", None)
           
            # Remove all keys containing these substrings
            for key in [*storable.keys()]:
                if any(ss in key.lower() for ss in ['position', 'rotation']):
                    storable.pop(key)
                   
            # Remove all storables whose ids contain these substrings
            if any(ss in storable['id'].lower() for ss in ["control", "trigger", "plugin", "preset", "animation"]):
                # print("pop", storable['id'])
                storables.remove(storable)
                continue
               
            # Remove transient morphs
            if storable['id'] == "geometry" and 'morphs' in storable:
                for morph in [*storable['morphs']]:
                    if any(re.match(ss, morph.get('uid', '')) for ss in [
                        r'Breast Impact',
                        r'^Eyelids (Top|Bottom) (Down|Up) (Left|Right)$', r'Brow .*(Up|Down)', r'^(Left|Right) Fingers'
                        r'^Mouth Open', r'Tongue In-Out', 'Shock', 'Surprise', 'Fear', 'Pain', 'Concentrate', 'Eyes Closed'
                    ]):
                        # print(morph)
                        storable['morphs'].remove(morph)
           
            # Remove storables with just an id and no properties
            if len(storable.keys()) == 1:
                # print("pop", storable['id'])
                storables.remove(storable)
                continue
        yield (atom, storables)


def saveAppearance(appearance, outname, readthumb, self_name):
    appearance_hash = json.dumps(appearance, sort_keys=True)
    appearance = appearance_hash.replace("SELF:", self_name + ":")
    if appearance_hash not in dupes:
        outpath = os.path.join(out_dir, outname + '.vap')
        with open(outpath, 'w') as fp:
            logging.info("-> %s", outpath)
            fp.write(appearance)
        try:
            with open(os.path.join(out_dir, outname + '.jpg'), 'wb') as fp:
                with readthumb() as fp2:
                    fp.write(fp2.read())
        except (KeyError, FileNotFoundError):
            # No image in archive
            pass
        dupes.add(appearance_hash)
    else:
        # logging.warn("Skip duplicate %s", outname)
        pass


def extractFromVar(var):
    __, filename = os.path.split(var)
    self_name, __ = os.path.splitext(filename)

    author = self_name.split('.')[0]

    try:
        with zipfile.ZipFile(var) as varzip:
            infodict = {
                zi.filename: zi
                for zi in varzip.infolist()
            }
            zip_scenes = {
                k: v for k, v in infodict.items()
                if k.startswith("Saves/scene") and k.endswith(".json")
            }
            for scenejsonpath in zip_scenes.keys():
                logging.debug("%s:%s", var, scenejsonpath)
               
                __, sjp_filename = os.path.split(scenejsonpath)
                sjp_plain, sjp_ext = os.path.splitext(sjp_filename)

                with varzip.open(scenejsonpath, 'r') as fp:
                    scenejson = json.load(fp)

                def readthumb():
                    return varzip.open(scenejsonpath.replace('.json', '.jpg'), 'r')

                def outnameFn(atom):
                    return f"Preset_{self_name}.{sjp_plain}{atom['id'].replace('Person', '')}".replace('#', '').replace('/', '-')

                extractFromSceneJson(scenejson, outnameFn, readthumb, self_name)
               
    except json.JSONDecodeError:
        print(var)
        traceback.print_exc()
        os.rename(var, var + '.invalid')
    except zipfile.BadZipFile:
        print(var)
        traceback.print_exc()
        os.rename(var, var + '.invalid')


def extractFromSceneJsonPath(scenejsonpath):
    __, sjp_filename = os.path.split(scenejsonpath)
    sjp_plain, sjp_ext = os.path.splitext(sjp_filename)

    def readthumb():
        return open(scenejsonpath.replace('.json', '.jpg'), 'rb')

    def outnameFn(atom):
        return f"Preset_!local.{sjp_plain}{atom['id'].replace('Person', '')}".replace('#', '').replace('/', '-')

    with open(scenejsonpath, 'r') as fp:
        extractFromSceneJson(json.load(fp), outnameFn, readthumb)


def extractFromSceneJson(scenejson, outnameFn, readthumb, self_name="SELF:"):
    # print([atom['id'] for atom in scenejson['atoms'] if atom['type'] == 'Person'])

    for atom, storables in getCleanedPersonAtoms(scenejson):
        # print(atom['id'])
        appearance = {
            "setUnlistedParamsToDefault": "true",
            'storables': storables
        }

        saveAppearance(appearance, outnameFn(atom), readthumb, self_name)


def main():
    if not sys.argv[1:]:
        print('Appearance Preset Extractor', SCRIPT_VERSION, 'by cartitolaire')
        print('')
        print('This tool automatically export presets from existing scenes and collects them in an "extracted" folder under presets. The presets will use the thumbnail for the source scene.')
        print('')
        print("Example usages:")
        print(' ', sys.argv[0], 'AddonPackages/Anon.SceneName.1.var')
        print(' ', sys.argv[0], '"AddonPackages/Anon*.var"')
        print(' ', sys.argv[0], '"Saves/scene/*.json" "AddonPackages/*.var"')
        return
   
    global out_dir
    if not os.path.isdir("./Custom/Atom/Person/"):
        logging.error("Script is not being run from the VaM root directory!")
        logging.warn("Can't cleanly merge into Person/Appearance presets; making ExtractedAppearance folder instead.")
        out_dir = "./ExtractedAppearance/"
    else:
        out_dir = "./Custom/Atom/Person/Appearance/extracted/"
       
    os.makedirs(out_dir, exist_ok=True)
   
    try:
        iterable = sum((glob.glob(a, recursive=True) for a in sys.argv[1:]), [])
        for filepath in tqdmfunc(iterable):
            __, filename = os.path.split(filepath)
            self_name, ext = os.path.splitext(filename)

            if ext == ".var":
                extractFromVar(filepath)
            elif ext == ".json":
                extractFromSceneJsonPath(filepath)
            else:
                raise NotImplementedError(ext)

    except KeyboardInterrupt:
        return

if __name__ == "__main__":
    main()
 
I was able to use the given PowerShell script to modify the Python script to replace SELF references with the actual dependency.

Wow @Tblot , I am going to test it tomorrow :) From what I can see, SELF replacement only works with extracting looks from VARS, but is not going to work with the option of extracting from JSONs, right?
 
Last edited:
Wow @Tblot , I am going to test it tomorrow :) From what I can see, SELF replacement only works with extracting looks from VARS, but is not going to work with the option of extracting from JSONs, right?

Yes. It might be possible to do it for JSONS as well but I don't know how. This one only works for .vars. Good eye!
 
Last edited:
It might be possible to do it for JSONS as well but I don't know how.
That's OK. And that's why my VAR extracting script that I shared, extracts JSONS and VAPS from VARS subdirectories that have names of the original VAR they were extracted from. Therefore, the PowerShell script (variable Directory.Name + ':') can use the directory name and replace SELF: with that name.
But now, with your script, I don't even have to extract VARS to take looks from scenes, so it does not matter in this case.
 
Patch to address the replacement of "SELF:/"
Diff:
--- extract_appearance.1.1.py.orig      2023-02-15 19:52:14.095994100 +0800
+++ extract_appearance.1.1~1.py 2023-02-15 22:39:36.782349300 +0800
@@ -83,6 +83,16 @@
         pass


+def update_dict(d, fn):
+    if isinstance(d, dict):
+        for k, v in d.items():
+                d[k] = update_dict(v, fn)
+        return d
+    elif isinstance(d, list):
+        return [ update_dict(i, fn) for i in d ]
+    else:
+        return fn(d)
+
 def extractFromVar(var):
     __, filename = os.path.split(var)
     self_name, __ = os.path.splitext(filename)
@@ -106,7 +116,7 @@
                 sjp_plain, sjp_ext = os.path.splitext(sjp_filename)

                 with varzip.open(scenejsonpath, 'r') as fp:
-                    scenejson = json.load(fp)
+                    scenejson = update_dict(json.load(fp), lambda v: v.replace("SELF:/", self_name + ":/"))

                 def readthumb():
                     return varzip.open(scenejsonpath.replace('.json', '.jpg'), 'r')
 
Could you make a guide how to apply this patch to the original file?
* Install `patch` command
- Method 1: Install git-for-windows: https://git-scm.com/download/win
- Method 2: Download and unzip the unix utils for windows: UnxUtils

* Save the content to a file "fix-self.patch"

* Open a command console and run below command:
`PATH\TO\patch.exe extract_appearance.1.1.py fix-self.patch`
 
Last edited:
It looks like where "SELF" occurs should walk to the .vaj file in the corresponding path and append the storables there to the vap's storables array?
 
1.2 update
Thank you, Sir :)

Now, I wonder how hard would it be to write a similar script, but to extract native animation from scene files, like the awesome plugin, Mocap Switcher, does?... ?
I am thinking about native one because I don't think extracting animations from Timeline would be even possible, as it is so often fragmented into multiple segments.[/QUOTE]
 
Yes, this worked for me. Thank you! I also forgot my VaM is on my external nvme drive so I needed to route the directory correctly.
How did it work for you? For me it keeps saying "can't find '__main__' module in 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\VaMX\\'"
 
Back
Top Bottom