From dd9ed69129f9e49c24e7041dd402912e34df7857 Mon Sep 17 00:00:00 2001 From: Anonymous <> Date: Tue, 25 Jul 2023 23:28:16 +0100 Subject: [PATCH] Add dynamic AI art --- css/art/genAI.css | 43 +++ js/002-config/fc-js-init.js | 3 +- js/003-data/gameVariableData.js | 8 +- src/004-base/basePrompt.js | 52 +++ src/art/artJS.js | 104 +++++- src/art/genAI/agePromptPart.js | 28 ++ src/art/genAI/arousalPromptPart.js | 22 ++ src/art/genAI/beautyPromptPart.js | 25 ++ src/art/genAI/breastsPromptPart.js | 37 +++ src/art/genAI/buildPrompt.js | 33 ++ src/art/genAI/clothesPromptPart.js | 461 ++++++++++++++++++++++++++ src/art/genAI/collarPromptPart.js | 20 ++ src/art/genAI/expressionPromptPart.js | 76 +++++ src/art/genAI/eyePromptPart.js | 19 ++ src/art/genAI/eyebrowPromptPart.js | 21 ++ src/art/genAI/hairPromptPart.js | 37 +++ src/art/genAI/healthPromptPart.js | 28 ++ src/art/genAI/heightPromptPart.js | 23 ++ src/art/genAI/hipsPromptPart.js | 35 ++ src/art/genAI/imageDB.js | 83 +++++ src/art/genAI/musclesPromptPart.js | 33 ++ src/art/genAI/piercingsPromptPart.js | 57 ++++ src/art/genAI/posturePromptPart.js | 39 +++ src/art/genAI/pubicHairPromptPart.js | 21 ++ src/art/genAI/racePromptPart.js | 18 + src/art/genAI/skinPromptPart.js | 79 +++++ src/art/genAI/stableDiffusion.js | 211 ++++++++++++ src/art/genAI/stylePromptPart.js | 15 + src/art/genAI/tattoosPromptPart.js | 33 ++ src/art/genAI/waistPromptPart.js | 31 ++ src/art/genAI/weightPromptPart.js | 35 ++ src/gui/options/options.js | 10 +- src/interaction/siCustom.js | 21 +- src/js/SlaveState.js | 7 + 34 files changed, 1754 insertions(+), 14 deletions(-) create mode 100644 css/art/genAI.css create mode 100644 src/004-base/basePrompt.js create mode 100644 src/art/genAI/agePromptPart.js create mode 100644 src/art/genAI/arousalPromptPart.js create mode 100644 src/art/genAI/beautyPromptPart.js create mode 100644 src/art/genAI/breastsPromptPart.js create mode 100644 src/art/genAI/buildPrompt.js create mode 100644 src/art/genAI/clothesPromptPart.js create mode 100644 src/art/genAI/collarPromptPart.js create mode 100644 src/art/genAI/expressionPromptPart.js create mode 100644 src/art/genAI/eyePromptPart.js create mode 100644 src/art/genAI/eyebrowPromptPart.js create mode 100644 src/art/genAI/hairPromptPart.js create mode 100644 src/art/genAI/healthPromptPart.js create mode 100644 src/art/genAI/heightPromptPart.js create mode 100644 src/art/genAI/hipsPromptPart.js create mode 100644 src/art/genAI/imageDB.js create mode 100644 src/art/genAI/musclesPromptPart.js create mode 100644 src/art/genAI/piercingsPromptPart.js create mode 100644 src/art/genAI/posturePromptPart.js create mode 100644 src/art/genAI/pubicHairPromptPart.js create mode 100644 src/art/genAI/racePromptPart.js create mode 100644 src/art/genAI/skinPromptPart.js create mode 100644 src/art/genAI/stableDiffusion.js create mode 100644 src/art/genAI/stylePromptPart.js create mode 100644 src/art/genAI/tattoosPromptPart.js create mode 100644 src/art/genAI/waistPromptPart.js create mode 100644 src/art/genAI/weightPromptPart.js diff --git a/css/art/genAI.css b/css/art/genAI.css new file mode 100644 index 00000000000..5e10009b5f1 --- /dev/null +++ b/css/art/genAI.css @@ -0,0 +1,43 @@ +.ai-art-image { + transition: filter 0.5s ease-in-out; + position: relative; +} + +.ai-art-container.refreshing .ai-art-image { + filter: blur(5px); +} + +.spinner { + display: none; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 30px; + animation: spin 2s linear infinite; +} + +.ai-art-container { + width: 100%; + height: 100%; + min-width: 100px; + min-height: 100px; + cursor: pointer; + float: right; + border: 3px hidden; + object-fit: contain; +} + +.ai-art-container.refreshing .spinner { + display: block; +} + +@keyframes spin { + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } +} \ No newline at end of file diff --git a/js/002-config/fc-js-init.js b/js/002-config/fc-js-init.js index 2197e4b88e3..5447e561135 100644 --- a/js/002-config/fc-js-init.js +++ b/js/002-config/fc-js-init.js @@ -2,7 +2,7 @@ // @ts-ignore "use strict"; -var App = { }; // eslint-disable-line no-redeclare +var App = {}; // eslint-disable-line no-redeclare // When adding namespace declarations, please consider needs of those using VSCode: // when you declare App.A{ A1:{}, A2:{} }, VSCode considers A, A1, and A2 to be @@ -15,6 +15,7 @@ var App = { }; // eslint-disable-line no-redeclare App.Arcology = {}; App.Arcology.Cell = {}; App.Art = {}; +App.Art.GenAI = {}; App.Budget = {}; App.Corporate = {}; App.Corporate.Division = {}; diff --git a/js/003-data/gameVariableData.js b/js/003-data/gameVariableData.js index 9de30128088..0e9234f13d0 100644 --- a/js/003-data/gameVariableData.js +++ b/js/003-data/gameVariableData.js @@ -99,6 +99,7 @@ App.Data.defaultGameStateVariables = { * | *3* | Revamped embedded vector art | * | *4* | Elohiem's WebGL | * | *5* | Shokushu's rendered | + * / *6* / Anon's AI image generation / */ imageChoice: 1, inbreeding: 1, @@ -168,6 +169,7 @@ App.Data.defaultGameStateVariables = { set3QView: false, seeAnimation: false, animFPS: 12, + aiApiUrl: "http://localhost:7860", showAgeDetail: 1, showAppraisal: 1, showAssignToScenes: 1, @@ -518,7 +520,7 @@ App.Data.resetOnNGPlus = { defaultRules: [], /** @type {Object.<string, number[]>} */ rulesToApplyOnce: {}, - raDefaultMode : 0, + raDefaultMode: 0, RECheckInIDs: [], @@ -1015,7 +1017,9 @@ App.Data.resetOnNGPlus = { /** @type {FC.SlaveStateOrZero} */ hostageWife: 0, /** @type {FC.Rival} */ - rival: {state: 0, duration: 0, prosperity: 0, power: 0, FS: {name: ""}, hostageState: 0}, + rival: { + state: 0, duration: 0, prosperity: 0, power: 0, FS: {name: ""}, hostageState: 0 + }, nationHate: 0, eventResults: {}, diff --git a/src/004-base/basePrompt.js b/src/004-base/basePrompt.js new file mode 100644 index 00000000000..55bb617dd12 --- /dev/null +++ b/src/004-base/basePrompt.js @@ -0,0 +1,52 @@ +/** base class for prompt parts */ +App.Art.GenAI.PromptPart = class PromptPart { + /** + * @param {FC.SlaveState} slave + */ + constructor(slave) { + this.slave = slave; + } + + /** + * @returns {string} + * @abstract + */ + positive() { + throw new Error("not implemented"); + } + + /** + * @returns {string} + * @abstract + */ + negative() { + throw new Error("not implemented"); + } +}; + +App.Art.GenAI.Prompt = class Prompt { + /** + * @param {App.Art.GenAI.PromptPart[]} parts + */ + constructor(parts) { + this.parts = parts; + } + + /** + * @returns {string} + */ + positive() { + let parts = this.parts.map(part => part.positive()); + parts = parts.filter(part => part); + return parts.join(", "); + } + + /** + * @returns {string} + */ + negative() { + let parts = this.parts.map(part => part.negative()); + parts = parts.filter(part => part); + return parts.join(", "); + } +}; diff --git a/src/art/artJS.js b/src/art/artJS.js index 1764d4a9d6f..a99ad1ea29d 100644 --- a/src/art/artJS.js +++ b/src/art/artJS.js @@ -134,6 +134,8 @@ App.Art.SlaveArtElement = function(artSlave, artSize, UIDisplay) { return App.Art.webglArtElement(artSlave, artSize); } else if (imageChoice === 5) { /* RENDERED IMAGES BY SHOKUSHU */ return App.Art.renderedArtElement(artSlave, artSize); + } else if (imageChoice === 6) { /* AI GENERATED IMAGES */ + return App.Art.aiArtElement(artSlave, artSize); } throw new Error(`imageChoice ${imageChoice} is out of range`); }; @@ -150,7 +152,7 @@ App.Art.setDynamicCSS = function(newState) { App.Art.dynamicCSS.innerHTML += '.smlImg { height: 150px; width: 150px }\n'; App.Art.dynamicCSS.innerHTML += '.medImg { height: 300px; width: 300px }\n'; - App.Art.dynamicCSS.innerHTML += '.lrgRender:not(.custom) { height: '+height+'px; width: '+width+'px;}\n'; + App.Art.dynamicCSS.innerHTML += '.lrgRender:not(.custom) { height: ' + height + 'px; width: ' + width + 'px;}\n'; App.Art.dynamicCSS.innerHTML += '.lrgRender > img { margin-left: auto; height: 530px; width: auto }\n'; } else { App.Art.dynamicCSS.innerHTML = ''; @@ -202,7 +204,7 @@ App.Art.webglInitialize = function() { try { // load model/morphs/textures assets let sceneData = App.Art.sceneGetData(); - App.Art.sceneGetData = function(){}; + App.Art.sceneGetData = function() { }; // load default dictionary containing camera/light/morph/material values let scene = App.Art.sceneGetParams(); scene.lockView = false; @@ -284,7 +286,7 @@ App.Art.webglArtElement = function(inSlave, artSize) { // it's common for e.g. events to make alterations to a slave just before rendering and revert them afterwards const slave = clone(inSlave); container.addEventListener("engineLoaded", function() { - new IntersectionObserver(function(entries) { // when visible in viewport + new IntersectionObserver(function(entries) { // when visible in viewport if (entries.some(e => e.isIntersecting)) { this.unobserve(container); // render only once @@ -426,6 +428,102 @@ App.Art.customArtElement = function(imageInfo, imageSize) { return res; }; +/** + * Render an AI generated image + * @param {App.Entity.SlaveState} slave - The slave whose image to render + * @param {number} imageSize - The size of the image to render + * @returns {Promise<HTMLElement>} Promise object that resolves with the created img element + */ +async function renderAIArt(slave, imageSize) { + let imgElement; + if (slave.custom.aiImageId === null) { + imgElement = document.createElement("div"); + } else { + imgElement = document.createElement("img"); + } + + imgElement.classList.add("ai-art-image"); + imgElement.setAttribute("style", "float:right; border:3px hidden; object-fit:contain; height:100%; width:100%;"); + + const sz = App.Art.artSizeToPx(imageSize); + if (sz) { + imgElement.setAttribute("width", sz); + imgElement.setAttribute("height", sz); + } + + try { + const imageData = await App.Art.GenAI.imageDB.getImage(slave.custom.aiImageId); + imgElement.setAttribute("src", imageData.data); + } catch (e) { + console.error(e); + } + + return imgElement; +} + +/** AI generated image that refreshes on click + * @param {App.Entity.SlaveState} slave + * @param {number} imageSize + * @returns {HTMLElement} + */ +App.Art.aiArtElement = function(slave, imageSize) { + const container = document.createElement("div"); + container.classList.add("ai-art-container"); + + /** + * @param {HTMLDivElement} container + */ + function makeSpinner(container) { + const spinner = document.createElement("div"); + spinner.classList.add("spinner"); + spinner.innerText = '⟳'; + container.appendChild(spinner); + } + makeSpinner(container); + + // Refresh on click + function refresh() { + renderAIArt(slave, imageSize).then((imgElement) => { + jQuery(container).empty().append(imgElement); + makeSpinner(container); + }); + } + + function updateAndRefresh() { + const imageGenerator = new App.Art.GenAI.StableDiffusionClient(V.aiApiUrl); + + container.classList.add("refreshing"); + + imageGenerator.updateSlave(slave).then(() => { + refresh(); + + container.classList.remove("refreshing"); + }).catch(error => { + console.error(error); + + container.classList.remove("refreshing"); + }); + } + + container.addEventListener("click", function() { + if (!container.classList.contains("refreshing")) { + updateAndRefresh(); + } + }); + + if (slave.custom.aiImageId === null) { + updateAndRefresh(); + } + + renderAIArt(slave, imageSize).then((imgElement) => { + jQuery(container).empty().append(imgElement); + makeSpinner(container); + }); + return container; +}; + + + /** * @param {number} artSize * @returns {string} diff --git a/src/art/genAI/agePromptPart.js b/src/art/genAI/agePromptPart.js new file mode 100644 index 00000000000..376ef513ac9 --- /dev/null +++ b/src/art/genAI/agePromptPart.js @@ -0,0 +1,28 @@ +App.Art.GenAI.AgePromptPart = class AgePromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + let ageTags = ``; + if (this.slave.visualAge < 18) { + ageTags = `teen, young, `; + } else if (this.slave.visualAge < 20) { + ageTags = `teen, `; + } + + return `${ageTags}${this.slave.visualAge} year old`; + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.visualAge < 20) { + return `old, 30 year old, 40 year old`; + } else if (this.slave.visualAge < 30) { /* empty */ } else if (this.slave.visualAge < 40) { + return `young, teen`; + } else { + return `young, teen, 20 year old`; + } + } +}; diff --git a/src/art/genAI/arousalPromptPart.js b/src/art/genAI/arousalPromptPart.js new file mode 100644 index 00000000000..a5f9435b042 --- /dev/null +++ b/src/art/genAI/arousalPromptPart.js @@ -0,0 +1,22 @@ +App.Art.GenAI.ArousalPromptPart = class ArousalPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.energy > 95) { + return "(blush, sweat, heavy breathing:1.1)"; + } else if (this.slave.energy > 80) { + return "blush, sweat, heavy breathing"; + } else if (this.slave.energy > 60) { + return "blush"; + } + return; + } + + /** + * @returns {string} + */ + negative() { + return undefined; + } +}; diff --git a/src/art/genAI/beautyPromptPart.js b/src/art/genAI/beautyPromptPart.js new file mode 100644 index 00000000000..c5b08e9ee63 --- /dev/null +++ b/src/art/genAI/beautyPromptPart.js @@ -0,0 +1,25 @@ +App.Art.GenAI.BeautyPromptPart = class BeautyPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.face < -95) { + return "ugly, low quality"; + } else if (this.slave.face < -50) { + return "unattractive, low quality"; + } else if (this.slave.face < 10) { /* empty */ } else if (this.slave.face < 50) { + return "best quality"; + } else if (this.slave.face < 95) { + return "masterpiece, best quality"; + } else { + return "(masterpiece, best quality:1.1)"; + } + } + + /** + * @returns {string} + */ + negative() { + return "(worst quality, low quality, bad_pictures, negative_hand-neg:1.2)"; + } +}; diff --git a/src/art/genAI/breastsPromptPart.js b/src/art/genAI/breastsPromptPart.js new file mode 100644 index 00000000000..269575e97d6 --- /dev/null +++ b/src/art/genAI/breastsPromptPart.js @@ -0,0 +1,37 @@ +App.Art.GenAI.BreastsPromptPart = class BreastsPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.boobs < 300) { + return `flat chest`; + } else if (this.slave.boobs < 400) { + return `small breasts, flat chest`; + } else if (this.slave.boobs < 500) { + return `small breasts`; + } else if (this.slave.boobs < 650) { + return `medium breasts`; + } else if (this.slave.boobs < 800) { + return `large breasts`; + } else if (this.slave.boobs < 1000) { + return `huge breasts`; + } else if (this.slave.boobs < 1400) { + return `huge breasts, large breasts`; + } else { + return `(huge breasts:1.1), large breasts`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.boobs < 300) { + return `medium breasts, large breasts, huge breasts`; + } else if (this.slave.boobs < 650) { + return; + } else { + return `small breasts, flat chest`; + } + } +}; diff --git a/src/art/genAI/buildPrompt.js b/src/art/genAI/buildPrompt.js new file mode 100644 index 00000000000..77e5d7d3187 --- /dev/null +++ b/src/art/genAI/buildPrompt.js @@ -0,0 +1,33 @@ +/** + * @param {FC.SlaveState} slave + * @returns {App.Art.GenAI.Prompt} + */ +// eslint-disable-next-line no-unused-vars +function buildPrompt(slave) { + let prompts = [ + new App.Art.GenAI.StylePromptPart(slave), + new App.Art.GenAI.BeautyPromptPart(slave), + new App.Art.GenAI.SkinPromptPart(slave), + new App.Art.GenAI.RacePromptPart(slave), + new App.Art.GenAI.AgePromptPart(slave), + new App.Art.GenAI.PosturePromptPart(slave), + new App.Art.GenAI.ArousalPromptPart(slave), + new App.Art.GenAI.WeightPromptPart(slave), + new App.Art.GenAI.HeightPromptPart(slave), + new App.Art.GenAI.MusclesPromptPart(slave), + new App.Art.GenAI.ClothesPromptPart(slave), + new App.Art.GenAI.CollarPromptPart(slave), + new App.Art.GenAI.BreastsPromptPart(slave), + new App.Art.GenAI.WaistPromptPart(slave), + new App.Art.GenAI.HipsPromptPart(slave), + new App.Art.GenAI.HairPromptPart(slave), + new App.Art.GenAI.EyePromptPart(slave), + new App.Art.GenAI.EyebrowPromptPart(slave), + new App.Art.GenAI.ExpressionPromptPart(slave), + new App.Art.GenAI.TattoosPromptPart(slave), + new App.Art.GenAI.PiercingsPromptPart(slave), + new App.Art.GenAI.HealthPromptPart(slave), + new App.Art.GenAI.PubicHairPromptPart(slave), + ]; + return new App.Art.GenAI.Prompt(prompts); +} diff --git a/src/art/genAI/clothesPromptPart.js b/src/art/genAI/clothesPromptPart.js new file mode 100644 index 00000000000..9ade4e51183 --- /dev/null +++ b/src/art/genAI/clothesPromptPart.js @@ -0,0 +1,461 @@ +const clothesPrompts = { + "no clothing": { + "positive": "(completely nude:1.1), pussy, nipples", + "negative": "clothes, jeans, underwear, pants, shorts, skirt, panties", + }, + "a Fuckdoll suit": { // Doesn't work well + "positive": "latex bodysuit, long sleeves", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "conservative clothing": { + "positive": "slacks, pants, silk blouse", + "negative": "jeans, nude, pussy, nipples", + }, + "chains": { + "positive": "(metal chains:1.1), nude, pussy, nipples, navel", + "negative": "clothes, jeans, underwear, pants, shorts, skirt, panties", + }, + "Western clothing": { + "positive": "flannel shirt, chaps, cowboy hat", + "negative": "nude, pussy, nipples", + }, + "body oil": { // Doesn't work well + "positive": "body oil, nude, pussy, nipples, navel", + "negative": "clothes, jeans, underwear, pants, shorts, skirt, panties", + }, + "a toga": { // Doesn't work well + "positive": "white toga", + "negative": "jeans, nude, pussy, nipples", + }, + "a huipil": { // Doesn't work well + "positive": "huipil, chinese clothing", + "negative": "jeans, nude, pussy, nipples", + }, + "a slutty qipao": { + "positive": "qipao, chinese clothing, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "spats and a tank top": { // Spats don't work well + "positive": "bike shorts, tank top", + "negative": "jeans, nude, pussy, nipples", + }, + "uncomfortable straps": { + "positive": "(leather straps, bondage:1.1), nude, pussy, nipples, navel", + "negative": "clothes, jeans, underwear, pants, shorts, skirt, panties", + }, + "shibari ropes": { + "positive": "shibari rope, bondage, nude, pussy, nipples, navel", + "negative": "clothes, jeans, underwear, pants, shorts, skirt, panties", + }, + "restrictive latex": { // Doesn't work well + "positive": "latex bodysuit, long sleeves", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "a latex catsuit": { // Doesn't work well + "positive": "latex bodysuit, long sleeves", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "attractive lingerie": { // Cupless part doesn't work well + "positive": "lingerie, cupless bra, nipples, thong", + "negative": "clothes, jeans, pants", + }, + "attractive lingerie for a pregnant woman": { // Cupless part doesn't work well + "positive": "lingerie, cupless bra, nipples, thong", + "negative": "clothes, jeans, pants", + }, + "kitty lingerie": { // Broken for photorealistic models, probably works for anime models + "positive": "cat lingerie, cat cutout, cat ear panties, bra, panties", + "negative": "cat ears, jeans, nude, pussy, nipples", + }, + "a maternity dress": { + "positive": "maternity dress, loose dress", + "negative": "jeans, nude, pussy, nipples", + }, + "stretch pants and a crop-top": { + "positive": "crop top, midriff, navel, leggings", + "negative": "jeans, nude, pussy, nipples", + }, + "a succubus outfit": { + "positive": "red leather corset, red leather miniskirt, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a fallen nuns habit": { + "positive": "(latex nun habit:1.1), thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a penitent nuns habit": { + "positive": "(latex nun habit:1.1), thighs, rope, bondage", + "negative": "jeans, nude, pussy, nipples", + }, + "a chattel habit": { + "positive": "(white gold latex nun:1.1), thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a string bikini": { // Cupless part doesn't work well + "positive": "string microbikini, cupless bikini, nipples", + "negative": "jeans, nude, pussy", + }, + "a scalemail bikini": { // Doesn't work well + "positive": "chainmail bikini, navel", + "negative": "jeans, nude, pussy, nipples", + }, + "striped panties": { + "positive": "blue striped panties, underwear only, nipples", + "negative": "jeans, nude, pussy", + }, + "a cheerleader outfit": { + "positive": "(cheerleader outfit:1.1), skirt, thighs, crop top, navel, midriff", + "negative": "jeans, nude, pussy, nipples", + }, + "clubslut netting": { // Doesn't work well + "positive": "nude, fishnets, nipples, pussy", + "negative": "cloth, jeans, pants, corset", + }, + "cutoffs and a t-shirt": { + "positive": "white t-shirt, jean shorts", + "negative": "nude, pussy, nipples", + }, + "slutty business attire": { + "positive": "suit jacket, cleavage, black skirt, thighs", + "negative": "jeans, nude, pussy, nipples" + }, + "nice business attire": { + "positive": "suit jacket, collared shirt, black skirt", + "negative": "jeans, nude, pussy, nipples", + }, + "a ball gown": { + "positive": "ballgown, long dress, luxurious dress, thighhighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a slave gown": { + "positive": "ballgown, long dress, luxurious dress, thighhighs, cleavage, see-through, translucent clothing, straps, bdsm", + "negative": "jeans, nude", + }, + "a halter top dress": { + "positive": "(halterneck:1.1), long dress, luxurious dress, bare back,", + "negative": "jeans, nude, pussy, nipples", + }, + "an evening dress": { + "positive": "evening gown, long dress, luxurious dress, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a mini dress": { + "positive": "short dress, tight dress, strapless, cleavage, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a comfortable bodysuit": { + "positive": "latex bodysuit, long sleeves", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "a leotard": { + "positive": "leotard, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a monokini": { // Doesn't work well + "positive": "monokini", + "negative": "jeans, nude, pussy, nipples", + }, + "an apron": { + "positive": "apron, thighs, nude", + "negative": "clothes, shirt, pants, shorts, pussy, nipples", + }, + "overalls": { + "positive": "overalls, naked overalls", + "negative": "shirt, pants, shorts, pussy, nipples, topless", + }, + "a cybersuit": { // Doesn't work well + "positive": "cybersuit, latex bodysuit, long sleeves, cybernetic, science fiction", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "a tight Imperial bodysuit": { // Doesn't work well + "positive": "imperial bodysuit, latex bodysuit, long sleeves, cybernetic, science fiction", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "battlearmor": { // Doesn't work well + "positive": "(armor, science fiction, soldier:1.1)", + "negative": "jeans, nude, pussy, nipples", + }, + "Imperial Plate": { // Doesn't work well + "positive": "(armor, science fiction, soldier:1.1)", + "negative": "jeans, nude, pussy, nipples", + }, + "a bunny outfit": { + "positive": "playboy bunny, backless leotard, pantyhose", + "negative": "jeans, nude, pussy, nipples, rabbit ears", + }, + "a slutty maid outfit": { + "positive": "maid, minidress, apron, white shirt, cleavage, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a nice maid outfit": { + "positive": "maid, dress, apron, white shirt", + "negative": "jeans, nude, pussy, nipples", + }, + "a slutty nurse outfit": { + "positive": "nurse, white jacket, cleavage, white skirt, thighs", + "negative": "jeans, shirt, pussy, nipples", + }, + "a nice nurse outfit": { + "positive": "nurse, white medical scrubs, pants", + "negative": "jeans, nude, pussy, nipples", + }, + "a dirndl": { + "positive": "(dirndl:1.1)", + "negative": "jeans, nude, pussy, nipples", + }, + "a long qipao": { + "positive": "(qipao:1.1), long dress, chinese clothes", + "negative": "jeans, nude, pussy, nipples", + }, + "lederhosen": { + "positive": "(lederhosen:1.1)", + "negative": "jeans, nude, pussy, nipples", + }, + "a biyelgee costume": { // Doesn't work well + "positive": "mongolian traditional clothes", + "negative": "jeans, nude, pussy, nipples", + }, + "a hanbok": { + "positive": "(hanbok:1.1)", + "negative": "jeans, nude, pussy, nipples", + }, + "burkini": { + "positive": "burqa, muslim clothes, burkini, pants", + "negative": "jeans, nude, pussy, nipples", + }, + "a hijab and blouse": { + "positive": "(hijab:1.1), blouse, short sleeves, long skirt", + "negative": "jeans, nude, pussy, nipples", + }, + "a hijab and abaya": { + "positive": "hijab, abaya", + "negative": "jeans, nude, pussy, nipples", + }, + "a niqab and abaya": { // Doesn't work well + "positive": "niqab, covered face, abaya", + "negative": "jeans, nude, pussy, nipples", + }, + "a burqa": { // Doesn't work well + "positive": "burqa, muslim clothes", + "negative": "jeans, nude, pussy, nipples", + }, + "a police uniform": { + "positive": "police uniform, policewoman, police hat, jacket, pants, belt", + "negative": "jeans, nude, pussy, nipples", + }, + "a gothic lolita dress": { + "positive": "gothic lolita, dress, thighhighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a one-piece swimsuit": { + "positive": "one-piece swimsuit, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a nice pony outfit": { // Tbh, not really sure what this is + "positive": "latex bodysuit, long sleeves", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "a slutty pony outfit": { // Same + "positive": "latex bodysuit, long sleeves, cleavage, thighs", + "negative": "nude, pussy, nipples", + }, + "a button-up shirt and panties": { // Often not bottomless + "positive": "collared shirt, oversized clothes, panties, (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nude, pussy, nipples", + }, + "a button-up shirt": { // Often not bottomless + "positive": "collared shirt, oversized clothes, pussy, nude, (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a sweater": { // Often not bottomless + "positive": "sweater, oversized clothes, pussy, nude, (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a t-shirt": { // Often not bottomless + "positive": "t-shirt, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a tank-top": { // Often not bottomless + "positive": "tank top, bare shoulders, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a tube top": { // Often not bottomless + "positive": "tube top, bare shoulders, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nude, nipples", + }, + "an oversized t-shirt": { // Often not bottomless + "positive": "t-shirt, oversized clothes, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a bra": { // Often not bottomless + "positive": "bra, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a sports bra": { // Often not bottomless + "positive": "sports bra, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a striped bra": { // Often not bottomless + "positive": "striped bra, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "pasties": { // Doesn't work well + "positive": "pasties, pussy, nude, (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a tube top and thong": { + "positive": "tube top, bare shoulders, (nude:1.1), (bottomless:1.1), g-string, thighs", + "negative": "jeans, pants, skirt, shorts, nipples, pussy", + }, + "a sweater and panties": { // Often not bottomless + "positive": "sweater, oversized clothes, panties, (nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples, pussy", + }, + "a tank-top and panties": { // Often not bottomless + "positive": "tank top, bare shoulders, panties, (nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples, pussy", + }, + "a t-shirt and thong": { // Often not bottomless + "positive": "t-shirt, (nude:1.1), (bottomless:1.1), g-string, thighs", + "negative": "jeans, pants, skirt, shorts, nipples, pussy", + }, + "an oversized t-shirt and boyshorts": { // Doesn't work well + "positive": "t-shirt, oversized clothes, boyshort panties, (nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, nipples, pussy", + }, + "sport shorts and a t-shirt": { + "positive": "t-shirt, sport shorts", + "negative": "jeans, pants, skirt, nipples, pussy", + }, + "sport shorts and a sports bra": { + "positive": "sports bra, sport shorts", + "negative": "jeans, pants, skirt, nipples, pussy", + }, + "a t-shirt and panties": { // Often not bottomless + "positive": "t-shirt, (nude:1.1), (bottomless:1.1), panties, thighs", + "negative": "jeans, pants, skirt, shorts, nipples, pussy", + }, + "striped underwear": { // Often not bottomless + "positive": "striped panties, striped bra", + "negative": "jeans, pants, skirt, shorts, nipples, pussy", + }, + "a thong": { + "positive": "thong, topless, nipples", + "negative": "jeans, pants, skirt, shorts, pussy", + }, + "a skimpy loincloth": { // Doesn't work well + "positive": "loincloth, topless, nipples", + "negative": "jeans, pants, skirt, shorts, pussy", + }, + "boyshorts": { + "positive": "boyshort panties, topless, nipples", + "negative": "jeans, pants, skirt, pussy", + }, + "panties": { + "positive": "panties, topless, nipples", + "negative": "jeans, pants, skirt, pussy", + }, + "panties and pasties": { // Doesn't work well + "positive": "panties, pasties, topless", + "negative": "jeans, pants, skirt, pussy, nipples", + }, + "cutoffs": { + "positive": "jean shorts, topless, nipples", + "negative": "pussy", + }, + "sport shorts": { + "positive": "sport shorts, topless, nipples", + "negative": "jeans, pants, skirt, pussy", + }, + "a sweater and cutoffs": { + "positive": "sweater, jean shorts", + "negative": "pussy, nipples", + }, + "leather pants and a tube top": { + "positive": "leather pants, tube top, bare shoulders", + "negative": "jeans, pants, skirt, shorts, pussy, nipples", + }, + "a t-shirt and jeans": { + "positive": "t-shirt, jeans", + "negative": "pussy, nipples", + }, + "leather pants and pasties": { // Doesn't work well + "positive": "leather pants, pasties, topless", + "negative": "jeans, pants, skirt, shorts, pussy, nipples", + }, + "leather pants": { + "positive": "leather pants, topless, nipples", + "negative": "jeans, pants, skirt, shorts, pussy", + }, + "jeans": { + "positive": "jeans, topless, nipples", + "negative": "pussy", + }, + "a military uniform": { + "positive": "military uniform, shirt, necktie, skirt", + "negative": "jeans, shorts, pussy, nipples", + }, + "battledress": { + "positive": "military fatigues, jumpsuit", + "negative": "jeans, shorts, pussy, nipples", + }, + "a mounty outfit": { // Doesn't work well + "positive": "mounty, red military jacket", + "negative": "jeans, shorts, pussy, nipples", + }, + "harem gauze": { + "positive": "harem outfit, loose dress, see-through, transparent clothes, nipples, pussy", + "negative": "jeans, shorts", + }, + "slutty jewelry": { + "positive": "nude, jewelry, gem, gold chains, armlet, nipples, pussy", + "negative": "clothes, jeans, underwear, pants, shorts, skirt, panties" + }, + "a Santa dress": { + "positive": "santa costume, santa dress, thighs", + "negative": "jeans, nude, pussy, nipples" + }, + "a bimbo outfit": { + "positive": "(pink:1.1) tube top, bra, cleavage, pink microskirt, thighs, panties, navel, midriff", + "negative": "jeans, nude, pussy, nipples", + }, + "a slutty outfit": { + "positive": "(pink:1.1) crop top, pink lowleg microskirt, (pussy:1.1), hip bones, groin, tight clothes, midriff, navel, (thighs:1.1)", + "negative": "jeans, nude, nipples", + }, + "a courtesan dress": { // Corset was messing stuff up, so I removed it + "positive": "(luxurious flowing dress:1.1), bare shoulders, long sleeves, detached sleeves", + "negative": "jeans, nude, pussy, nipples", + }, + "a schoolgirl outfit": { + "positive": "school uniform, white shirt, plaid skirt", + "negative": "jeans, nude, pussy, nipples", + } +}; + +App.Art.GenAI.ClothesPromptPart = class ClothesPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + getClothes() { + let clothes = this.slave.clothes; + if (!clothesPrompts.hasOwnProperty(clothes)) { + clothes = "no clothing"; + } + return clothes; + } + + /** + * @returns {string} + */ + positive() { + return clothesPrompts[this.getClothes()].positive; + } + + /** + * @returns {string} + */ + negative() { + return clothesPrompts[this.getClothes()].negative; + } +}; diff --git a/src/art/genAI/collarPromptPart.js b/src/art/genAI/collarPromptPart.js new file mode 100644 index 00000000000..dd0fe004286 --- /dev/null +++ b/src/art/genAI/collarPromptPart.js @@ -0,0 +1,20 @@ +App.Art.GenAI.CollarPromptPart = class CollarPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.collar !== "none") { + return `${this.slave.collar} collar`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.collar === "none") { + return "collar"; + } + return; + } +}; diff --git a/src/art/genAI/expressionPromptPart.js b/src/art/genAI/expressionPromptPart.js new file mode 100644 index 00000000000..56abc5cc624 --- /dev/null +++ b/src/art/genAI/expressionPromptPart.js @@ -0,0 +1,76 @@ +App.Art.GenAI.ExpressionPromptPart = class ExpressionPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + let devotionPart; + if (this.slave.devotion < -50) { + devotionPart = `angry expression, hateful`; + } else if (this.slave.devotion < -20) { + devotionPart = `angry`; + } else if (this.slave.devotion < 51) { + devotionPart = null; + } else if (this.slave.devotion < 95) { + devotionPart = `smile`; + } else { + devotionPart = `smile, grin, teeth, loving expression`; + } + + let trustPart; + if (this.slave.trust < -90) { + trustPart = `(scared expression:1.2), looking down, crying, tears`; + } + if (this.slave.trust < -50) { + trustPart = `(scared expression:1.1), looking down, crying`; + } else if (this.slave.trust < -20) { + trustPart = `scared expression, looking down`; + } else if (this.slave.trust < 51) { + trustPart = `looking at viewer`; + } else if (this.slave.trust < 95) { + trustPart = `looking at viewer, confident`; + } else { + trustPart = `looking at viewer, confident, smirk`; + } + + if (devotionPart && trustPart) { + return `(${devotionPart}, ${trustPart}:1.1)`; + } else if (devotionPart) { + return `(${devotionPart}:1.1)`; + } else if (trustPart) { + return `(${trustPart}:1.1)`; + } + } + + /** + * @returns {string} + */ + negative() { + let devotionPart; + if (this.slave.devotion < -50) { + devotionPart = `smile, loving expression`; + } else if (this.slave.devotion < -20) { + devotionPart = `smile`; + } else if (this.slave.devotion < 51) { + devotionPart = null; + } else { + devotionPart = `angry`; + } + + let trustPart; + if (this.slave.trust < -50) { + trustPart = `looking at viewer, confident`; + } else if (this.slave.trust < -20) { + trustPart = null; + } else { + trustPart = `looking away`; + } + + if (devotionPart && trustPart) { + return `${devotionPart}, ${trustPart}`; + } else if (devotionPart) { + return devotionPart; + } else if (trustPart) { + return trustPart; + } + } +}; diff --git a/src/art/genAI/eyePromptPart.js b/src/art/genAI/eyePromptPart.js new file mode 100644 index 00000000000..e99d2bb5438 --- /dev/null +++ b/src/art/genAI/eyePromptPart.js @@ -0,0 +1,19 @@ +App.Art.GenAI.EyePromptPart = class EyePromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.eye.left.iris === this.slave.eye.right.iris) { + return `${this.slave.eye.left.iris} eyes`; + } else { + return `heterochromia, ${this.slave.eye.left.iris} left eye, ${this.slave.eye.right.iris} right eye`; + } + } + + /** + * @returns {string} + */ + negative() { + return undefined; + } +}; diff --git a/src/art/genAI/eyebrowPromptPart.js b/src/art/genAI/eyebrowPromptPart.js new file mode 100644 index 00000000000..d8ac6bbdce7 --- /dev/null +++ b/src/art/genAI/eyebrowPromptPart.js @@ -0,0 +1,21 @@ +App.Art.GenAI.EyebrowPromptPart = class EyebrowPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.eyebrowHStyle === "shaved" || this.slave.eyebrowHStyle === "bald" || this.slave.eyebrowHStyle === "hairless") { + return; + } + return `${this.slave.eyebrowFullness} ${this.slave.eyebrowHColor} eyebrows`; + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.eyebrowHStyle === "shaved" || this.slave.eyebrowHStyle === "bald" || this.slave.eyebrowHStyle === "hairless") { + return "eyebrows"; + } + return; + } +}; diff --git a/src/art/genAI/hairPromptPart.js b/src/art/genAI/hairPromptPart.js new file mode 100644 index 00000000000..b48071d00fb --- /dev/null +++ b/src/art/genAI/hairPromptPart.js @@ -0,0 +1,37 @@ +App.Art.GenAI.HairPromptPart = class HairPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.bald || this.slave.hLength === 0) { + return `bald`; + } + + const heightVhLength = this.slave.hLength / this.slave.height; + let hairLength = ''; + if (heightVhLength > 0.9) { + hairLength = `(very long:1.2)`; + } else if (heightVhLength > 0.7) { + hairLength = `(very long:1.1)`; + } else if (heightVhLength >= 0.4) { + hairLength = `very long`; + } else if (heightVhLength >= 0.2) { + hairLength = `long`; + } else if (this.slave.hLength >= 15) { + hairLength = `medium`; + } else { + hairLength = `short`; + } + return `${this.slave.hStyle} hair, ${hairLength} hair, ${this.slave.hColor} hair`; + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.bald || this.slave.hLength === 0) { + return `hair, long hair, short hair`; + } + return; + } +}; diff --git a/src/art/genAI/healthPromptPart.js b/src/art/genAI/healthPromptPart.js new file mode 100644 index 00000000000..e0437b1e8fc --- /dev/null +++ b/src/art/genAI/healthPromptPart.js @@ -0,0 +1,28 @@ +App.Art.GenAI.HealthPromptPart = class HealthPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.health.condition < -90) { + return `(very sick, ill:1.1)`; + } else if (this.slave.health.condition < -50) { + return `sick, ill`; + } else if (this.slave.health.condition < -10) { + return `tired`; + } else if (this.slave.health.condition < 90) { + return null; + } else { + return `healthy`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.health.condition > 50) { + return `sick, ill`; + } + return; + } +}; diff --git a/src/art/genAI/heightPromptPart.js b/src/art/genAI/heightPromptPart.js new file mode 100644 index 00000000000..180bd73314f --- /dev/null +++ b/src/art/genAI/heightPromptPart.js @@ -0,0 +1,23 @@ +App.Art.GenAI.HeightPromptPart = class HeightPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.height < 150) { + return `short`; + } else if (this.slave.height > 180) { + return `tall`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.height < 150) { + return `tall`; + } else if (this.slave.height > 180) { + return `short`; + } + } +}; diff --git a/src/art/genAI/hipsPromptPart.js b/src/art/genAI/hipsPromptPart.js new file mode 100644 index 00000000000..ecb0182031b --- /dev/null +++ b/src/art/genAI/hipsPromptPart.js @@ -0,0 +1,35 @@ +App.Art.GenAI.HipsPromptPart = class HipsPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.hips <= -2) { + return `(narrow hips:1.1)`; + } else if (this.slave.hips === -1) { + return `narrow hips`; + } else if (this.slave.hips === 0) { + return null; + } else if (this.slave.hips === 1) { + return `hips`; + } else if (this.slave.hips === 2) { + return `wide hips`; + } else { + return `(wide hips:1.1)`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.hips <= -2) { + return `hips, wide hips`; + } else if (this.slave.hips === -1) { + return `hips`; + } else if (this.slave.hips === 0) { + return null; + } else { + return `narrow hips`; + } + } +}; diff --git a/src/art/genAI/imageDB.js b/src/art/genAI/imageDB.js new file mode 100644 index 00000000000..992c4872043 --- /dev/null +++ b/src/art/genAI/imageDB.js @@ -0,0 +1,83 @@ +App.Art.GenAI.imageDB = (function() { + let db; + + /** + * Create an IndexedDB and initialize objectStore if it doesn't already exist. + * @returns {Promise<IDBDatabase>} Promise object that resolves with the opened database + */ + async function createDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open('AIImages', 1); + + request.onerror = function() { + console.log('Database failed to open'); + reject('Database failed to open'); + }; + + request.onsuccess = function() { + console.log('Database opened successfully'); + db = request.result; + resolve(db); + }; + + request.onupgradeneeded = function(e) { + // @ts-ignore + let db = e.target.result; + db.createObjectStore('images', {keyPath: 'id', autoIncrement: true}); + }; + }); + } + + /** + * Add an image to the IndexedDB + * @param {Object} imageData - The image data to store + * @returns {Promise<number>} Promise object that resolves with the ID of the stored image + */ + async function addImage(imageData) { + return new Promise((resolve, reject) => { + let transaction = db.transaction(['images'], 'readwrite'); + let objectStore = transaction.objectStore('images'); + + let request = objectStore.add(imageData); + + request.onsuccess = function() { + resolve(request.result); + }; + + transaction.oncomplete = function() { + console.log('Transaction completed: database modification finished.'); + }; + + transaction.onerror = function() { + console.log('Transaction not opened due to error'); + reject('Transaction not opened due to error'); + }; + }); + } + + /** + * Get an image from the IndexedDB + * @param {number} id - The ID of the image to retrieve + * @returns {Promise<Object>} Promise object that resolves with the retrieved image data + */ + async function getImage(id) { + return new Promise((resolve, reject) => { + let transaction = db.transaction(['images'], 'readonly'); + let objectStore = transaction.objectStore('images'); + + let request = objectStore.get(id); + + request.onsuccess = function() { + resolve(request.result); + }; + }); + } + + return { + createDB, + addImage, + getImage, + }; +})(); + +App.Art.GenAI.imageDB.createDB(); diff --git a/src/art/genAI/musclesPromptPart.js b/src/art/genAI/musclesPromptPart.js new file mode 100644 index 00000000000..50ead82aa51 --- /dev/null +++ b/src/art/genAI/musclesPromptPart.js @@ -0,0 +1,33 @@ +App.Art.GenAI.MusclesPromptPart = class MusclesPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.muscles > 95) { + return `(muscular:1.3)`; + } else if (this.slave.muscles > 30) { + return `(muscular:1.2)`; + } else if (this.slave.muscles > 10) { + return `muscular`; + } else if (this.slave.muscles > -10) { + return null; + } else if (this.slave.muscles > -95) { + return `soft`; + } else { + return `frail, weak`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.muscles > 30) { + return `soft`; + } else if (this.slave.muscles > -10) { + return null; + } else if (this.slave.muscles > -30) { + return `muscular`; + } + } +}; diff --git a/src/art/genAI/piercingsPromptPart.js b/src/art/genAI/piercingsPromptPart.js new file mode 100644 index 00000000000..e760be542a0 --- /dev/null +++ b/src/art/genAI/piercingsPromptPart.js @@ -0,0 +1,57 @@ +App.Art.GenAI.PiercingsPromptPart = class PiercingsPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + let piercingParts = []; + if (this.slave.piercing.areola.weight > 0) { + let desc = this.slave.piercing.areola.desc ? (this.slave.piercing.areola.desc + ` `) : ``; + piercingParts.push(`${desc}areola piercing`); + } + if (this.slave.piercing.ear.weight > 0) { + let desc = this.slave.piercing.ear.desc ? (this.slave.piercing.ear.desc + ` `) : ``; + piercingParts.push(`${desc}ear piercing`); + } + if (this.slave.piercing.eyebrow.weight > 0) { + let desc = this.slave.piercing.eyebrow.desc ? (this.slave.piercing.eyebrow.desc + ` `) : ``; + piercingParts.push(`${desc}eyebrow piercing`); + } + if (this.slave.piercing.lips.weight > 0) { + let desc = this.slave.piercing.lips.desc ? (this.slave.piercing.lips.desc + ` `) : ``; + piercingParts.push(`${desc}lip piercing`); + } + if (this.slave.piercing.navel.weight > 0) { + let desc = this.slave.piercing.navel.desc ? (this.slave.piercing.navel.desc + ` `) : ``; + piercingParts.push(`${desc}navel piercing`); + } + if (this.slave.piercing.nipple.weight > 0) { + let desc = this.slave.piercing.nipple.desc ? (this.slave.piercing.nipple.desc + ` `) : ``; + piercingParts.push(`${desc}nipple piercing`); + } + if (this.slave.piercing.nose.weight > 0) { + let desc = this.slave.piercing.nose.desc ? (this.slave.piercing.nose.desc + ` `) : ``; + piercingParts.push(`${desc}nose piercing`); + } + if (this.slave.piercing.tongue.weight > 0) { + let desc = this.slave.piercing.tongue.desc ? (this.slave.piercing.tongue.desc + ` `) : ``; + piercingParts.push(`${desc}tongue piercing`); + } + if (this.slave.piercing.vagina.weight > 0) { + let desc = this.slave.piercing.vagina.desc ? (this.slave.piercing.vagina.desc + ` `) : ``; + piercingParts.push(`${desc}labia piercing`); + } + + if (piercingParts.length > 0) { + return piercingParts.join(`, `); + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.piercing.areola.weight === 0 && this.slave.piercing.ear.weight === 0 && this.slave.piercing.eyebrow.weight === 0 && this.slave.piercing.genitals.weight === 0 && this.slave.piercing.lips.weight === 0 && this.slave.piercing.navel.weight === 0 && this.slave.piercing.nipple.weight === 0 && this.slave.piercing.nose.weight === 0 && this.slave.piercing.tongue.weight === 0 && this.slave.piercing.vagina.weight === 0) { + return `piercings`; + } + } +}; diff --git a/src/art/genAI/posturePromptPart.js b/src/art/genAI/posturePromptPart.js new file mode 100644 index 00000000000..9653828fe56 --- /dev/null +++ b/src/art/genAI/posturePromptPart.js @@ -0,0 +1,39 @@ +App.Art.GenAI.PosturePromptPart = class PosturePromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + let devotionPart; + if (this.slave.devotion < -50) { + devotionPart = `standing, from side, arms crossed`; + } else if (this.slave.devotion < -20) { + devotionPart = `standing, arms crossed`; + } else if (this.slave.devotion < 21) { + devotionPart = `standing`; + } else { + devotionPart = `standing, arms behind back`; + } + + let trustPart; + if (this.slave.trust < -50) { + trustPart = `trembling, head down`; + } else if (this.slave.trust < -20) { + trustPart = `trembling`; + } + + if (devotionPart && trustPart) { + return `${devotionPart}, ${trustPart}`; + } else if (devotionPart) { + return devotionPart; + } else if (trustPart) { + return trustPart; + } + } + + /** + * @returns {string} + */ + negative() { + return undefined; + } +}; diff --git a/src/art/genAI/pubicHairPromptPart.js b/src/art/genAI/pubicHairPromptPart.js new file mode 100644 index 00000000000..c82b735f78e --- /dev/null +++ b/src/art/genAI/pubicHairPromptPart.js @@ -0,0 +1,21 @@ +App.Art.GenAI.PubicHairPromptPart = class PubicHairPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.pubicHStyle === "waxed" || this.slave.pubicHStyle === "bald" || this.slave.pubicHStyle === "hairless") { + return; + } + return `${this.slave.pubicHColor} ${this.slave.pubicHStyle} pubic hair`; + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.pubicHStyle === "waxed" || this.slave.pubicHStyle === "bald" || this.slave.pubicHStyle === "hairless") { + return "pubic hair"; + } + return; + } +}; diff --git a/src/art/genAI/racePromptPart.js b/src/art/genAI/racePromptPart.js new file mode 100644 index 00000000000..dda8562160f --- /dev/null +++ b/src/art/genAI/racePromptPart.js @@ -0,0 +1,18 @@ +App.Art.GenAI.RacePromptPart = class RacePromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + return this.slave.race; + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.race !== "asian") { + return "asian"; + } + return; + } +}; diff --git a/src/art/genAI/skinPromptPart.js b/src/art/genAI/skinPromptPart.js new file mode 100644 index 00000000000..8bedb7ed739 --- /dev/null +++ b/src/art/genAI/skinPromptPart.js @@ -0,0 +1,79 @@ +App.Art.GenAI.SkinPromptPart = class SkinPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + switch (this.slave.skin) { + case "pure white": + case "ivory": + case "white": + case "extremely pale": + case "very pale": + case "pale": + return "pale skin"; + case "extremely fair": + case "very fair": + case "fair": + case "light": + case "light olive": + break; + case "sun tanned": + case "spray tanned": + case "tan": + case "olive": + case "bronze": + case "dark olive": + case "dark": + case "light beige": + case "beige": + case "dark beige": + case "light brown": + case "brown": + return "dark skin"; + case "dark brown": + case "black": + case "ebony": + case "pure black": + return "black skin"; + } + } + + /** + * @returns {string} + */ + negative() { + switch (this.slave.skin) { + case "pure white": + case "ivory": + case "white": + case "extremely pale": + case "very pale": + case "pale": + case "extremely fair": + case "very fair": + case "fair": + case "light": + case "light olive": + return "dark skin"; + case "sun tanned": + case "spray tanned": + case "tan": + case "olive": + return; + case "bronze": + case "dark olive": + case "dark": + case "light beige": + case "beige": + case "dark beige": + case "light brown": + case "brown": + case "dark brown": + case "black": + case "ebony": + case "pure black": + return "light skin"; + } + } +}; + diff --git a/src/art/genAI/stableDiffusion.js b/src/art/genAI/stableDiffusion.js new file mode 100644 index 00000000000..4d4bfcde4a1 --- /dev/null +++ b/src/art/genAI/stableDiffusion.js @@ -0,0 +1,211 @@ +/* eslint-disable camelcase */ +App.Art.GenAI.StableDiffusionSettings = class { + /** + * @typedef {Object} ConstructorOptions + * @param {boolean} [enable_hr=true] + * @param {number} [denoising_strength=0.6] + * @param {number} [firstphase_width=400] + * @param {number} [firstphase_height=768] + * @param {number} [hr_scale=1.7] + * @param {string} [hr_upscaler="Latent"] + * @param {number} [hr_second_pass_steps=12] + * @param {number} [hr_resize_x=0] + * @param {number} [hr_resize_y=0] + * @param {string} [hr_sampler_name="DPM++ 2M SDE Karras"] + * @param {string} [hr_prompt=null] + * @param {string} [hr_negative_prompt=null] + * @param {string} [prompt=""] + * @param {number} [seed=1337] + * @param {string} [sampler_name="DPM++ 2M SDE Karras"] + * @param {number} [batch_size=1] + * @param {number} [n_iter=1] + * @param {number} [steps=20] + * @param {number} [cfg_scale=5.5] + * @param {number} [width=400] + * @param {number} [height=768] + * @param {string} [negative_prompt=""] + * @param {number} [eta=1.0] + * @param {number} [s_min_uncond=0.0] + * @param {number} [s_churn=0.0] + * @param {number} [s_tmax=0.0] + * @param {number} [s_tmin=0.0] + * @param {number} [s_noise=0] + * @param {string[]} [override_settings=["Discard penultimate sigma: True"]] + * @param {boolean} [override_settings_restore_afterwards=true] + */ + + /** + * @param {ConstructorOptions} options The options for the constructor. + */ + constructor({ + enable_hr = true, + denoising_strength = 0.6, + firstphase_width = 400, + firstphase_height = 768, + hr_scale = 1.7, + hr_upscaler = "Latent", + hr_second_pass_steps = 12, + hr_resize_x = 0, + hr_resize_y = 0, + hr_sampler_name = "DPM++ 2M SDE Karras", + hr_prompt = null, + hr_negative_prompt = null, + prompt = "", + seed = 1337, + sampler_name = "DPM++ 2M SDE Karras", + batch_size = 1, + n_iter = 1, + steps = 20, + cfg_scale = 5.5, + width = 400, + height = 768, + negative_prompt = "", + eta = 1.0, + s_min_uncond = 0.0, + s_churn = 0.0, + s_tmax = 0.0, + s_tmin = 0.0, + s_noise = 0, + override_settings = { + "always_discard_next_to_last_sigma": true, + }, + override_settings_restore_afterwards = true, + } = {}) { + this.enable_hr = enable_hr; + this.denoising_strength = denoising_strength; + this.firstphase_width = firstphase_width; + this.firstphase_height = firstphase_height; + this.hr_scale = hr_scale; + this.hr_upscaler = hr_upscaler; + this.hr_second_pass_steps = hr_second_pass_steps; + this.hr_resize_x = hr_resize_x; + this.hr_resize_y = hr_resize_y; + this.hr_sampler_name = hr_sampler_name; + this.hr_prompt = hr_prompt; + this.hr_negative_prompt = hr_negative_prompt; + this.prompt = prompt; + this.seed = seed; + this.sampler_name = sampler_name; + this.batch_size = batch_size; + this.n_iter = n_iter; + this.steps = steps; + this.cfg_scale = cfg_scale; + this.width = width; + this.height = height; + this.negative_prompt = negative_prompt; + this.eta = eta; + this.s_min_uncond = s_min_uncond; + this.s_churn = s_churn; + this.s_tmax = s_tmax; + this.s_tmin = s_tmin; + this.s_noise = s_noise; + this.override_settings = override_settings; + this.override_settings_restore_afterwards = override_settings_restore_afterwards; + } +}; + + +/** + * @param {string} url + * @param {number} timeout + * @param {Object} [options] + * @returns {Promise<Response>} + */ +async function fetchWithTimeout(url, timeout, options) { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + const response = await fetch(url, {signal: controller.signal, ...options}); + clearTimeout(id); + return response; +} + + +App.Art.GenAI.StableDiffusionClient = class { + /** + * @param {string} apiUrl + */ + constructor(apiUrl) { + this.apiUrl = apiUrl; + } + + /** + * @param {App.Art.GenAI.StableDiffusionSettings} settings + * @returns {Promise<string>} - Base 64 encoded image (could be a jpeg or png) + */ + async fetchImage(settings) { + const options = { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(settings), + }; + + const response = await fetchWithTimeout(`${this.apiUrl}/sdapi/v1/txt2img`, 30000, options); + if (!response.ok) { + console.error("Error fetching Stable Diffusion image", response); + throw new Error(`Error fetching Stable Diffusion image - status: ${response.status}`); + } + + let parsedRes = await response.json(); + return parsedRes.images[0]; + } + + /** + * @param {FC.SlaveState} slave + * @returns {App.Art.GenAI.StableDiffusionSettings} + */ + buildStableDiffusionSettings(slave) { + const prompt = buildPrompt(slave); + const settings = new App.Art.GenAI.StableDiffusionSettings({ + prompt: prompt.positive(), + hr_prompt: prompt.positive(), + negative_prompt: prompt.negative(), + hr_negative_prompt: prompt.negative(), + seed: slave.natural.artSeed, + }); + + return settings; + } + + /** + * @param {FC.SlaveState} slave + * @returns {Promise<string>} - Base 64 encoded image (could be a jpeg or png) + */ + async fetchImageForSlave(slave) { + const settings = this.buildStableDiffusionSettings(slave); + return this.fetchImage(settings); + } + + /** + * Update a slave object with a new image + * @param {FC.SlaveState} slave - The slave to update + */ + async updateSlave(slave) { + const base64Image = await this.fetchImageForSlave(slave); + const mimeType = getMimeType(base64Image); + + const imageId = await App.Art.GenAI.imageDB.addImage({data: `data:${mimeType};base64,${base64Image}`}); + slave.custom.aiImageId = imageId; + } +}; + +/** + * @param {string} base64Image + * @returns {string} + */ +function getMimeType(base64Image) { + const jpegCheck = "/9j/"; + const pngCheck = "iVBOR"; + const webpCheck = "UklGR"; + + if (base64Image.startsWith(jpegCheck)) { + return "image/jpeg"; + } else if (base64Image.startsWith(pngCheck)) { + return "image/png"; + } else if (base64Image.startsWith(webpCheck)) { + return "image/webp"; + } else { + return "unknown"; + } +} diff --git a/src/art/genAI/stylePromptPart.js b/src/art/genAI/stylePromptPart.js new file mode 100644 index 00000000000..4b76eb204c9 --- /dev/null +++ b/src/art/genAI/stylePromptPart.js @@ -0,0 +1,15 @@ +App.Art.GenAI.StylePromptPart = class StylePromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + return "<lora:FilmVelvia3:0.6> <lora:LowRA:0.5> 8k photography, dark theme, black background"; + } + + /** + * @returns {string} + */ + negative() { + return "greyscale, monochrome, cg, render, unreal engine"; + } +}; diff --git a/src/art/genAI/tattoosPromptPart.js b/src/art/genAI/tattoosPromptPart.js new file mode 100644 index 00000000000..6811e2d7012 --- /dev/null +++ b/src/art/genAI/tattoosPromptPart.js @@ -0,0 +1,33 @@ +App.Art.GenAI.TattoosPromptPart = class TattoosPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + let tattooParts = []; + if (this.slave.armsTat) { + tattooParts.push(`${this.slave.armsTat} arm tattoo`); + } + if (this.slave.legsTat) { + tattooParts.push(`${this.slave.legsTat} leg tattoo`); + } + if (this.slave.bellyTat) { + tattooParts.push(`${this.slave.bellyTat} belly tattoo`); + } + if (this.slave.boobsTat) { + tattooParts.push(`${this.slave.boobsTat} breast tattoo`); + } + + if (tattooParts.length > 0) { + return tattooParts.join(', '); + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.armsTat || this.slave.legsTat || this.slave.bellyTat || this.slave.boobsTat) { + return `tattoo`; + } + } +}; diff --git a/src/art/genAI/waistPromptPart.js b/src/art/genAI/waistPromptPart.js new file mode 100644 index 00000000000..016896bea14 --- /dev/null +++ b/src/art/genAI/waistPromptPart.js @@ -0,0 +1,31 @@ +App.Art.GenAI.WaistPromptPart = class WaistPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.waist > 95) { + return `very wide waist`; + } else if (this.slave.waist > 10) { + return `wide waist`; + } else if (this.slave.waist > -40) { + return null; + } else if (this.slave.waist > -95) { + return `narrow waist`; + } else { + return `(narrow waist:1.1)`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.waist > 30) { + return `narrow waist`; + } else if (this.slave.waist > -30) { + return null; + } else if (this.slave.waist > -95) { + return `wide waist`; + } + } +}; diff --git a/src/art/genAI/weightPromptPart.js b/src/art/genAI/weightPromptPart.js new file mode 100644 index 00000000000..9a55f1fae98 --- /dev/null +++ b/src/art/genAI/weightPromptPart.js @@ -0,0 +1,35 @@ +App.Art.GenAI.WeightPromptPart = class WeightPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.weight < -95) { + return `emaciated, very thin, skinny`; + } else if (this.slave.weight < -30) { + return `very thin, skinny`; + } else if (this.slave.weight < -10) { + return `slim`; + } else if (this.slave.weight < 10) { + return null; + } else if (this.slave.weight < 30) { + return `curvy`; + } else if (this.slave.weight < 95) { + return `plump, chubby`; + } else { + return `fat, obese, plump`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.weight < -30) { + return `plump, chubby`; + } else if (this.slave.weight < 50) { + return null; + } else { + return `thin, skinny`; + } + } +}; diff --git a/src/gui/options/options.js b/src/gui/options/options.js index 51dde4f01e9..e67062537a7 100644 --- a/src/gui/options/options.js +++ b/src/gui/options/options.js @@ -492,7 +492,7 @@ App.UI.optionsPassage = function() { option.addComment("Enable cheat mode to edit genetics."); } - options.addCustomOption("Rules Assistant").addButton("Reset Rules", () => { initRules(); }, "Rules Assistant"); + options.addCustomOption("Rules Assistant").addButton("Reset Rules", () => {initRules();}, "Rules Assistant"); options.addOption("Passage Profiler is", "profiler") .addValue("Enabled", 1).on().addValue("Disabled", 0).off() @@ -737,10 +737,6 @@ App.UI.optionsPassage = function() { options.addOption("New event", "tempEventToggle") .addValue("Enabled", 1).on().addValue("Disabled", 0).off(); - options.addOption("New Interactions", "interactions", V.experimental) - .addValue("Enabled", 1).on().addValue("Disabled", 0).off() - .addComment("This will enable WIP slave interactions. Currently fully functional, but a little rough around the edges."); - options.addOption("Sex overhaul", "sexOverhaul", V.experimental) .addValue("Enabled", 1).on().addValue("Disabled", 0).off() .addComment("This will enable a new way to interact with slaves. Currently working but missing flavor text."); @@ -1144,6 +1140,7 @@ App.UI.artOptions = function() { ["Revamped embedded vector art", 3], ["Non-embedded vector art", 2], ["Shokushu's rendered image pack", 5], + ["Anon's AI image generation", 6], ]); if (V.imageChoice === 1) { options.addComment("The only 2D art in somewhat recent development. Contains many outfits."); @@ -1227,6 +1224,9 @@ App.UI.artOptions = function() { .addValue("6", 6).off().addValue("12", 12).on().addValue("24", 24).off().addValue("32", 32).off(); } else if (V.imageChoice === 2) { option.addComment("This art development is dead since vanilla. Since it is not embedded, requires a separate art pack to be downloaded."); + } else if (V.imageChoice === 6) { + options.addComment("This is highly experimental. If you're reading this, tell me to replace it with instructions on how to set up Stable Diffusion."); + options.addOption("API URL", "aiApiUrl").showTextBox().addComment("The URL of the Automatic 1111 Stable Diffusion API."); } options.addOption("PA avatar art is", "seeAvatar") diff --git a/src/interaction/siCustom.js b/src/interaction/siCustom.js index 793b3720a03..40a30adefc3 100644 --- a/src/interaction/siCustom.js +++ b/src/interaction/siCustom.js @@ -17,7 +17,8 @@ App.UI.SlaveInteract.custom = function(slave, refresh) { el.append( customSlaveImage(), customHairImage(), - artSeed() + artSeed(), + genAIPrompt() ); App.UI.DOM.appendNewElement("h3", el, `Names`); @@ -688,8 +689,8 @@ App.UI.SlaveInteract.custom = function(slave, refresh) { function artSeed() { const frag = new DocumentFragment(); - if (V.imageChoice === 4) { // webGL only right now - App.UI.DOM.appendNewElement("p", frag, `WebGL rendering uses a "seed value" to make small changes to the appearance of your slaves. If you're dissatisfied with this slave's appearance and correcting ${his} physical parameters doesn't seem to help, you can try replacing the seed value. Slaves with identical seeds will look identical; the game carefully preserves this value for clones and identical twins, but if you change it here it becomes your responsibility.`); + if (V.imageChoice === 4 || V.imageChoice === 6) { // webGL and AI art only right now + App.UI.DOM.appendNewElement("p", frag, `Some rendering methods use a "seed value" to make small changes to the appearance of your slaves. If you're dissatisfied with this slave's appearance and correcting ${his} physical parameters doesn't seem to help, you can try replacing the seed value. Slaves with identical seeds will look identical; the game carefully preserves this value for clones and identical twins, but if you change it here it becomes your responsibility.`); const setArtSeed = (/** @type {number} */ num) => { slave.natural.artSeed = num; @@ -823,4 +824,18 @@ App.UI.SlaveInteract.custom = function(slave, refresh) { return el; } + + function genAIPrompt() { + let el = document.createElement('p'); + + el.appendChild(document.createElement('h4')).textContent = `Image generation AI (eg. Stable Diffusion):`; + + let prompt = buildPrompt(slave); + el.appendChild(document.createElement('h5')).textContent = `Positive prompt`; + el.appendChild(document.createElement('kbd')).textContent = prompt.positive(); + el.appendChild(document.createElement('h5')).textContent = `Negative prompt`; + el.appendChild(document.createElement('kbd')).textContent = prompt.negative(); + + return el; + } }; diff --git a/src/js/SlaveState.js b/src/js/SlaveState.js index 38db8b14537..f7e61cb97f1 100644 --- a/src/js/SlaveState.js +++ b/src/js/SlaveState.js @@ -380,6 +380,13 @@ App.Entity.SlaveCustomAddonsState = class SlaveCustomAddonsState { * @type {FC.Zeroable<string>} */ this.hairVector = 0; + /** + * holds the ai image ID + * + * used if ai images are enabled + * @type {FC.Zeroable<number>} + */ + this.aiImageId = null; } }; -- GitLab