2023-07-20 20:32:15 +03:00
import {
substituteParams ,
saveSettingsDebounced ,
systemUserName ,
hideSwipeButtons ,
showSwipeButtons ,
callPopup ,
getRequestHeaders ,
event _types ,
eventSource ,
2023-07-22 21:12:23 +03:00
appendImageToMessage ,
2023-07-22 23:57:48 +03:00
generateQuietPrompt ,
this _chid ,
2023-07-20 20:32:15 +03:00
} from "../../../script.js" ;
import { getApiUrl , getContext , extension _settings , doExtrasFetch , modules } from "../../extensions.js" ;
2023-07-22 23:57:48 +03:00
import { selected _group } from "../../group-chats.js" ;
import { stringFormat , initScrollHeight , resetScrollHeight , timestampToMoment , getCharaFilename } from "../../utils.js" ;
2023-07-20 20:32:15 +03:00
export { MODULE _NAME } ;
// Wraps a string into monospace font-face span
const m = x => ` <span class="monospace"> ${ x } </span> ` ;
// Joins an array of strings with ' / '
const j = a => a . join ( ' / ' ) ;
// Wraps a string into paragraph block
const p = a => ` <p> ${ a } </p> `
const MODULE _NAME = 'sd' ;
const UPDATE _INTERVAL = 1000 ;
const postHeaders = {
'Content-Type' : 'application/json' ,
'Bypass-Tunnel-Reminder' : 'bypass' ,
} ;
const generationMode = {
CHARACTER : 0 ,
USER : 1 ,
SCENARIO : 2 ,
RAW _LAST : 3 ,
NOW : 4 ,
FACE : 5 ,
FREE : 6 ,
}
2023-07-22 21:12:23 +03:00
const modeLabels = {
[ generationMode . CHARACTER ] : 'Character ("Yourself")' ,
[ generationMode . FACE ] : 'Portrait ("Your Face")' ,
[ generationMode . USER ] : 'User ("Me")' ,
[ generationMode . SCENARIO ] : 'Scenario ("The Whole Story")' ,
[ generationMode . NOW ] : 'Last Message' ,
[ generationMode . RAW _LAST ] : 'Raw Last Message' ,
}
2023-07-20 20:32:15 +03:00
const triggerWords = {
[ generationMode . CHARACTER ] : [ 'you' ] ,
[ generationMode . USER ] : [ 'me' ] ,
[ generationMode . SCENARIO ] : [ 'scene' ] ,
[ generationMode . RAW _LAST ] : [ 'raw_last' ] ,
[ generationMode . NOW ] : [ 'last' ] ,
[ generationMode . FACE ] : [ 'face' ] ,
}
2023-07-22 21:12:23 +03:00
const promptTemplates = {
2023-07-20 20:32:15 +03:00
/*OLD: [generationMode.CHARACTER]: "Pause your roleplay and provide comma-delimited list of phrases and keywords which describe {{char}}'s physical appearance and clothing. Ignore {{char}}'s personality traits, and chat history when crafting this description. End your response once the comma-delimited list is complete. Do not roleplay when writing this description, and do not attempt to continue the story.", */
[ generationMode . CHARACTER ] : "[In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase 'full body portrait,']" ,
//face-specific prompt
[ generationMode . FACE ] : "[In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, facial features and expressions, occupation, hair and hair accessories (if any), what they are wearing on their upper body (if anything). Do not describe anything below their neck. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase 'close up facial portrait,']" ,
//prompt for only the last message
[ generationMode . USER ] : "[Pause your roleplay and provide a detailed description of {{user}}'s physical appearance from the perspective of {{char}} in the form of a comma-delimited list of keywords and phrases. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase 'full body portrait,'. Ignore the rest of the story when crafting this description. Do not roleplay as {{char}} when writing this description, and do not attempt to continue the story.]" ,
[ generationMode . SCENARIO ] : "[Pause your roleplay and provide a detailed description for all of the following: a brief recap of recent events in the story, {{char}}'s appearance, and {{char}}'s surroundings. Do not roleplay while writing this description.]" ,
[ generationMode . NOW ] : ` [Pause your roleplay. Your next response must be formatted as a single comma-delimited list of concise keywords. The list will describe of the visual details included in the last chat message.
Only mention characters by using pronouns ( 'he' , 'his' , 'she' , 'her' , 'it' , 'its' ) or neutral nouns ( 'male' , 'the man' , 'female' , 'the woman' ) .
Ignore non - visible things such as feelings , personality traits , thoughts , and spoken dialog .
Add keywords in this precise order :
a keyword to describe the location of the scene ,
a keyword to mention how many characters of each gender or type are present in the scene ( minimum of two characters :
{ { user } } and { { char } } , example : '2 men ' or '1 man 1 woman ' , '1 man 3 robots' ) ,
keywords to describe the relative physical positioning of the characters to each other ( if a commonly known term for the positioning is known use it instead of describing the positioning in detail ) + 'POV' ,
a single keyword or phrase to describe the primary act taking place in the last chat message ,
keywords to describe { { char } } ' s physical appearance and facial expression ,
keywords to describe { { char } } ' s actions ,
keywords to describe { { user } } ' s physical appearance and actions .
If character actions involve direct physical interaction with another character , mention specifically which body parts interacting and how .
A correctly formatted example response would be :
'(location),(character list by gender),(primary action), (relative character position) POV, (character 1' s description and actions ) , ( character 2 's description and actions)' ] ` ,
[ generationMode . RAW _LAST ] : "[Pause your roleplay and provide ONLY the last chat message string back to me verbatim. Do not write anything after the string. Do not roleplay at all in your response. Do not continue the roleplay story.]" ,
}
const helpString = [
` ${ m ( '(argument)' ) } – requests SD to make an image. Supported arguments: ` ,
'<ul>' ,
` <li> ${ m ( j ( triggerWords [ generationMode . CHARACTER ] ) ) } – AI character full body selfie</li> ` ,
` <li> ${ m ( j ( triggerWords [ generationMode . FACE ] ) ) } – AI character face-only selfie</li> ` ,
` <li> ${ m ( j ( triggerWords [ generationMode . USER ] ) ) } – user character full body selfie</li> ` ,
` <li> ${ m ( j ( triggerWords [ generationMode . SCENARIO ] ) ) } – visual recap of the whole chat scenario</li> ` ,
` <li> ${ m ( j ( triggerWords [ generationMode . NOW ] ) ) } – visual recap of the last chat message</li> ` ,
` <li> ${ m ( j ( triggerWords [ generationMode . RAW _LAST ] ) ) } – visual recap of the last chat message with no summary</li> ` ,
'</ul>' ,
` Anything else would trigger a "free mode" to make SD generate whatever you prompted.<Br>
example : '/sd apple tree' would generate a picture of an apple tree . ` ,
] . join ( '<br>' ) ;
const defaultSettings = {
// CFG Scale
scale _min : 1 ,
scale _max : 30 ,
scale _step : 0.5 ,
scale : 7 ,
// Sampler steps
steps _min : 1 ,
steps _max : 150 ,
steps _step : 1 ,
steps : 20 ,
// Image dimensions (Width & Height)
dimension _min : 64 ,
dimension _max : 2048 ,
dimension _step : 64 ,
width : 512 ,
height : 512 ,
prompt _prefix : 'best quality, absurdres, masterpiece,' ,
negative _prompt : 'lowres, bad anatomy, bad hands, text, error, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry' ,
sampler : 'DDIM' ,
model : '' ,
// Automatic1111/Horde exclusives
restore _faces : false ,
enable _hr : false ,
// Horde settings
horde : false ,
horde _nsfw : false ,
horde _karras : true ,
// Refine mode
refine _mode : false ,
2023-07-22 21:12:23 +03:00
prompts : promptTemplates ,
2023-07-20 20:32:15 +03:00
}
async function loadSettings ( ) {
if ( Object . keys ( extension _settings . sd ) . length === 0 ) {
Object . assign ( extension _settings . sd , defaultSettings ) ;
}
2023-07-22 21:12:23 +03:00
if ( extension _settings . sd . prompts === undefined ) {
extension _settings . sd . prompts = promptTemplates ;
}
2023-07-22 23:57:48 +03:00
if ( extension _settings . sd . character _prompts === undefined ) {
extension _settings . sd . character _prompts = { } ;
}
2023-07-20 20:32:15 +03:00
$ ( '#sd_scale' ) . val ( extension _settings . sd . scale ) . trigger ( 'input' ) ;
$ ( '#sd_steps' ) . val ( extension _settings . sd . steps ) . trigger ( 'input' ) ;
$ ( '#sd_prompt_prefix' ) . val ( extension _settings . sd . prompt _prefix ) . trigger ( 'input' ) ;
$ ( '#sd_negative_prompt' ) . val ( extension _settings . sd . negative _prompt ) . trigger ( 'input' ) ;
$ ( '#sd_width' ) . val ( extension _settings . sd . width ) . trigger ( 'input' ) ;
$ ( '#sd_height' ) . val ( extension _settings . sd . height ) . trigger ( 'input' ) ;
$ ( '#sd_horde' ) . prop ( 'checked' , extension _settings . sd . horde ) ;
$ ( '#sd_horde_nsfw' ) . prop ( 'checked' , extension _settings . sd . horde _nsfw ) ;
$ ( '#sd_horde_karras' ) . prop ( 'checked' , extension _settings . sd . horde _karras ) ;
$ ( '#sd_restore_faces' ) . prop ( 'checked' , extension _settings . sd . restore _faces ) ;
$ ( '#sd_enable_hr' ) . prop ( 'checked' , extension _settings . sd . enable _hr ) ;
$ ( '#sd_refine_mode' ) . prop ( 'checked' , extension _settings . sd . refine _mode ) ;
2023-07-22 21:12:23 +03:00
addPromptTemplates ( ) ;
2023-07-20 20:32:15 +03:00
await Promise . all ( [ loadSamplers ( ) , loadModels ( ) ] ) ;
}
2023-07-22 21:12:23 +03:00
function addPromptTemplates ( ) {
$ ( '#sd_prompt_templates' ) . empty ( ) ;
for ( const [ name , prompt ] of Object . entries ( extension _settings . sd . prompts ) ) {
const label = $ ( '<label></label>' )
. text ( modeLabels [ name ] )
. attr ( 'for' , ` sd_prompt_ ${ name } ` ) ;
const textarea = $ ( '<textarea></textarea>' )
2023-07-22 23:57:48 +03:00
. addClass ( 'textarea_compact text_pole' )
2023-07-22 21:12:23 +03:00
. attr ( 'id' , ` sd_prompt_ ${ name } ` )
. attr ( 'rows' , 6 )
. val ( prompt ) . on ( 'input' , ( ) => {
extension _settings . sd . prompts [ name ] = textarea . val ( ) ;
saveSettingsDebounced ( ) ;
} ) ;
const button = $ ( '<button></button>' )
. addClass ( 'menu_button fa-solid fa-undo' )
. attr ( 'title' , 'Restore default' )
. on ( 'click' , ( ) => {
textarea . val ( promptTemplates [ name ] ) ;
extension _settings . sd . prompts [ name ] = promptTemplates [ name ] ;
saveSettingsDebounced ( ) ;
} ) ;
const container = $ ( '<div></div>' )
. addClass ( 'title_restorable' )
. append ( label )
. append ( button )
$ ( '#sd_prompt_templates' ) . append ( container ) ;
$ ( '#sd_prompt_templates' ) . append ( textarea ) ;
}
}
2023-07-22 20:32:58 +03:00
async function refinePrompt ( prompt ) {
if ( extension _settings . sd . refine _mode ) {
const refinedPrompt = await callPopup ( '<h3>Review and edit the prompt:</h3>Press "Cancel" to abort the image generation.' , 'input' , prompt , { rows : 5 , okButton : 'Generate' } ) ;
if ( refinedPrompt ) {
return refinedPrompt ;
} else {
throw new Error ( 'Generation aborted by user.' ) ;
}
}
return prompt ;
}
2023-07-22 23:57:48 +03:00
function onChatChanged ( ) {
if ( this _chid === undefined || selected _group ) {
$ ( '#sd_character_prompt_block' ) . hide ( ) ;
return ;
}
$ ( '#sd_character_prompt_block' ) . show ( ) ;
const key = getCharaFilename ( this _chid ) ;
$ ( '#sd_character_prompt' ) . val ( key ? ( extension _settings . sd . character _prompts [ key ] || '' ) : '' ) ;
}
function onCharacterPromptInput ( ) {
const key = getCharaFilename ( this _chid ) ;
extension _settings . sd . character _prompts [ key ] = $ ( '#sd_character_prompt' ) . val ( ) ;
2023-07-23 00:28:23 +03:00
resetScrollHeight ( $ ( this ) ) ;
2023-07-22 23:57:48 +03:00
saveSettingsDebounced ( ) ;
}
function getCharacterPrefix ( ) {
if ( selected _group ) {
return '' ;
}
const key = getCharaFilename ( this _chid ) ;
if ( key ) {
return extension _settings . sd . character _prompts [ key ] || '' ;
}
return '' ;
}
function combinePrefixes ( str1 , str2 ) {
if ( ! str2 ) {
return str1 ;
}
// Remove leading/trailing white spaces and commas from the strings
str1 = str1 . trim ( ) . replace ( /^,|,$/g , '' ) ;
str2 = str2 . trim ( ) . replace ( /^,|,$/g , '' ) ;
// Combine the strings with a comma between them
var result = ` ${ str1 } , ${ str2 } , ` ;
return result ;
}
2023-07-20 20:32:15 +03:00
function onRefineModeInput ( ) {
extension _settings . sd . refine _mode = ! ! $ ( '#sd_refine_mode' ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
function onScaleInput ( ) {
extension _settings . sd . scale = Number ( $ ( '#sd_scale' ) . val ( ) ) ;
$ ( '#sd_scale_value' ) . text ( extension _settings . sd . scale . toFixed ( 1 ) ) ;
saveSettingsDebounced ( ) ;
}
function onStepsInput ( ) {
extension _settings . sd . steps = Number ( $ ( '#sd_steps' ) . val ( ) ) ;
$ ( '#sd_steps_value' ) . text ( extension _settings . sd . steps ) ;
saveSettingsDebounced ( ) ;
}
function onPromptPrefixInput ( ) {
extension _settings . sd . prompt _prefix = $ ( '#sd_prompt_prefix' ) . val ( ) ;
resetScrollHeight ( $ ( this ) ) ;
saveSettingsDebounced ( ) ;
}
function onNegativePromptInput ( ) {
extension _settings . sd . negative _prompt = $ ( '#sd_negative_prompt' ) . val ( ) ;
resetScrollHeight ( $ ( this ) ) ;
saveSettingsDebounced ( ) ;
}
function onSamplerChange ( ) {
extension _settings . sd . sampler = $ ( '#sd_sampler' ) . find ( ':selected' ) . val ( ) ;
saveSettingsDebounced ( ) ;
}
function onWidthInput ( ) {
extension _settings . sd . width = Number ( $ ( '#sd_width' ) . val ( ) ) ;
$ ( '#sd_width_value' ) . text ( extension _settings . sd . width ) ;
saveSettingsDebounced ( ) ;
}
function onHeightInput ( ) {
extension _settings . sd . height = Number ( $ ( '#sd_height' ) . val ( ) ) ;
$ ( '#sd_height_value' ) . text ( extension _settings . sd . height ) ;
saveSettingsDebounced ( ) ;
}
async function onHordeInput ( ) {
extension _settings . sd . model = null ;
extension _settings . sd . sampler = null ;
extension _settings . sd . horde = ! ! $ ( this ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
await Promise . all ( [ loadModels ( ) , loadSamplers ( ) ] ) ;
}
async function onHordeNsfwInput ( ) {
extension _settings . sd . horde _nsfw = ! ! $ ( this ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
async function onHordeKarrasInput ( ) {
extension _settings . sd . horde _karras = ! ! $ ( this ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
function onRestoreFacesInput ( ) {
extension _settings . sd . restore _faces = ! ! $ ( this ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
function onHighResFixInput ( ) {
extension _settings . sd . enable _hr = ! ! $ ( this ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
}
async function onModelChange ( ) {
extension _settings . sd . model = $ ( '#sd_model' ) . find ( ':selected' ) . val ( ) ;
saveSettingsDebounced ( ) ;
if ( ! extension _settings . sd . horde ) {
await updateExtrasRemoteModel ( ) ;
}
}
async function updateExtrasRemoteModel ( ) {
const url = new URL ( getApiUrl ( ) ) ;
url . pathname = '/api/image/model' ;
const getCurrentModelResult = await doExtrasFetch ( url , {
method : 'POST' ,
headers : postHeaders ,
body : JSON . stringify ( { model : extension _settings . sd . model } ) ,
} ) ;
if ( getCurrentModelResult . ok ) {
console . log ( 'Model successfully updated on SD remote.' ) ;
}
}
async function loadSamplers ( ) {
$ ( '#sd_sampler' ) . empty ( ) ;
let samplers = [ ] ;
if ( extension _settings . sd . horde ) {
samplers = await loadHordeSamplers ( ) ;
} else {
samplers = await loadExtrasSamplers ( ) ;
}
for ( const sampler of samplers ) {
const option = document . createElement ( 'option' ) ;
option . innerText = sampler ;
option . value = sampler ;
option . selected = sampler === extension _settings . sd . sampler ;
$ ( '#sd_sampler' ) . append ( option ) ;
}
}
async function loadHordeSamplers ( ) {
const result = await fetch ( '/horde_samplers' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
} ) ;
if ( result . ok ) {
const data = await result . json ( ) ;
return data ;
}
return [ ] ;
}
async function loadExtrasSamplers ( ) {
if ( ! modules . includes ( 'sd' ) ) {
return [ ] ;
}
const url = new URL ( getApiUrl ( ) ) ;
url . pathname = '/api/image/samplers' ;
const result = await doExtrasFetch ( url ) ;
if ( result . ok ) {
const data = await result . json ( ) ;
return data . samplers ;
}
return [ ] ;
}
async function loadModels ( ) {
$ ( '#sd_model' ) . empty ( ) ;
let models = [ ] ;
if ( extension _settings . sd . horde ) {
models = await loadHordeModels ( ) ;
} else {
models = await loadExtrasModels ( ) ;
}
for ( const model of models ) {
const option = document . createElement ( 'option' ) ;
option . innerText = model . text ;
option . value = model . value ;
option . selected = model . value === extension _settings . sd . model ;
$ ( '#sd_model' ) . append ( option ) ;
}
}
async function loadHordeModels ( ) {
const result = await fetch ( '/horde_models' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
} ) ;
if ( result . ok ) {
const data = await result . json ( ) ;
data . sort ( ( a , b ) => b . count - a . count ) ;
const models = data . map ( x => ( { value : x . name , text : ` ${ x . name } (ETA: ${ x . eta } s, Queue: ${ x . queued } , Workers: ${ x . count } ) ` } ) ) ;
return models ;
}
return [ ] ;
}
async function loadExtrasModels ( ) {
if ( ! modules . includes ( 'sd' ) ) {
return [ ] ;
}
const url = new URL ( getApiUrl ( ) ) ;
url . pathname = '/api/image/model' ;
const getCurrentModelResult = await doExtrasFetch ( url ) ;
if ( getCurrentModelResult . ok ) {
const data = await getCurrentModelResult . json ( ) ;
extension _settings . sd . model = data . model ;
}
url . pathname = '/api/image/models' ;
const getModelsResult = await doExtrasFetch ( url ) ;
if ( getModelsResult . ok ) {
const data = await getModelsResult . json ( ) ;
const view _models = data . models . map ( x => ( { value : x , text : x } ) ) ;
return view _models ;
}
return [ ] ;
}
function getGenerationType ( prompt ) {
for ( const [ key , values ] of Object . entries ( triggerWords ) ) {
for ( const value of values ) {
if ( value . toLowerCase ( ) === prompt . toLowerCase ( ) . trim ( ) ) {
return Number ( key ) ;
}
}
}
return generationMode . FREE ;
}
function getQuietPrompt ( mode , trigger ) {
if ( mode === generationMode . FREE ) {
return trigger ;
}
2023-07-22 21:12:23 +03:00
return substituteParams ( stringFormat ( extension _settings . sd . prompts [ mode ] , trigger ) ) ;
2023-07-20 20:32:15 +03:00
}
function processReply ( str ) {
if ( ! str ) {
return '' ;
}
str = str . replaceAll ( '"' , '' )
str = str . replaceAll ( '“' , '' )
str = str . replaceAll ( '.' , ',' )
str = str . replaceAll ( '\n' , ', ' )
str = str . replace ( /[^a-zA-Z0-9,:()]+/g , ' ' ) // Replace everything except alphanumeric characters and commas with spaces
str = str . replace ( /\s+/g , ' ' ) ; // Collapse multiple whitespaces into one
str = str . trim ( ) ;
str = str
. split ( ',' ) // list split by commas
. map ( x => x . trim ( ) ) // trim each entry
. filter ( x => x ) // remove empty entries
. join ( ', ' ) ; // join it back with proper spacing
return str ;
}
function getRawLastMessage ( ) {
const context = getContext ( ) ;
const lastMessage = context . chat . slice ( - 1 ) [ 0 ] . mes ,
characterDescription = context . characters [ context . characterId ] . description ,
situation = context . characters [ context . characterId ] . scenario ;
return ` (( ${ processReply ( lastMessage ) } )), ( ${ processReply ( situation ) } :0.7), ( ${ processReply ( characterDescription ) } :0.5) `
}
async function generatePicture ( _ , trigger , message , callback ) {
if ( ! trigger || trigger . trim ( ) . length === 0 ) {
console . log ( 'Trigger word empty, aborting' ) ;
return ;
}
if ( ! modules . includes ( 'sd' ) && ! extension _settings . sd . horde ) {
toastr . warning ( "Extensions API is not connected or doesn't provide SD module. Enable Stable Horde to generate images." ) ;
return ;
}
extension _settings . sd . sampler = $ ( '#sd_sampler' ) . find ( ':selected' ) . val ( ) ;
extension _settings . sd . model = $ ( '#sd_model' ) . find ( ':selected' ) . val ( ) ;
trigger = trigger . trim ( ) ;
const generationType = getGenerationType ( trigger ) ;
console . log ( 'Generation mode' , generationType , 'triggered with' , trigger ) ;
const quiet _prompt = getQuietPrompt ( generationType , trigger ) ;
const context = getContext ( ) ;
const prevSDHeight = extension _settings . sd . height ;
if ( generationType == generationMode . FACE ) {
2023-07-27 23:38:08 +03:00
// Round to nearest multiple of 64
extension _settings . sd . height = Math . round ( extension _settings . sd . height * 1.5 / 64 ) * 64 ;
2023-07-20 20:32:15 +03:00
}
try {
const prompt = await getPrompt ( generationType , message , trigger , quiet _prompt ) ;
console . log ( 'Processed Stable Diffusion prompt:' , prompt ) ;
context . deactivateSendButtons ( ) ;
hideSwipeButtons ( ) ;
await sendGenerationRequest ( prompt , callback ) ;
} catch ( err ) {
console . trace ( err ) ;
throw new Error ( 'SD prompt text generation failed.' )
}
finally {
extension _settings . sd . height = prevSDHeight ;
context . activateSendButtons ( ) ;
showSwipeButtons ( ) ;
}
}
async function getPrompt ( generationType , message , trigger , quiet _prompt ) {
let prompt ;
switch ( generationType ) {
case generationMode . RAW _LAST :
prompt = message || getRawLastMessage ( ) ;
break ;
case generationMode . FREE :
prompt = trigger . trim ( ) ;
break ;
default :
prompt = await generatePrompt ( quiet _prompt ) ;
break ;
}
2023-07-22 20:32:58 +03:00
if ( generationType !== generationMode . FREE ) {
prompt = await refinePrompt ( prompt ) ;
}
2023-07-20 20:32:15 +03:00
return prompt ;
}
async function generatePrompt ( quiet _prompt ) {
2023-07-22 21:12:23 +03:00
const reply = await generateQuietPrompt ( quiet _prompt ) ;
return processReply ( reply ) ;
2023-07-20 20:32:15 +03:00
}
async function sendGenerationRequest ( prompt , callback ) {
if ( extension _settings . sd . horde ) {
await generateHordeImage ( prompt , callback ) ;
} else {
await generateExtrasImage ( prompt , callback ) ;
}
}
async function generateExtrasImage ( prompt , callback ) {
console . log ( extension _settings . sd ) ;
const url = new URL ( getApiUrl ( ) ) ;
url . pathname = '/api/image' ;
const result = await doExtrasFetch ( url , {
method : 'POST' ,
headers : postHeaders ,
body : JSON . stringify ( {
prompt : prompt ,
sampler : extension _settings . sd . sampler ,
steps : extension _settings . sd . steps ,
scale : extension _settings . sd . scale ,
width : extension _settings . sd . width ,
height : extension _settings . sd . height ,
2023-07-22 23:57:48 +03:00
prompt _prefix : combinePrefixes ( extension _settings . sd . prompt _prefix , getCharacterPrefix ( ) ) ,
2023-07-20 20:32:15 +03:00
negative _prompt : extension _settings . sd . negative _prompt ,
restore _faces : ! ! extension _settings . sd . restore _faces ,
enable _hr : ! ! extension _settings . sd . enable _hr ,
karras : ! ! extension _settings . sd . horde _karras ,
} ) ,
} ) ;
if ( result . ok ) {
const data = await result . json ( ) ;
const base64Image = ` data:image/jpeg;base64, ${ data . image } ` ;
callback ? callback ( prompt , base64Image ) : sendMessage ( prompt , base64Image ) ;
} else {
callPopup ( 'Image generation has failed. Please try again.' , 'text' ) ;
}
}
async function generateHordeImage ( prompt , callback ) {
const result = await fetch ( '/horde_generateimage' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( {
prompt : prompt ,
sampler : extension _settings . sd . sampler ,
steps : extension _settings . sd . steps ,
scale : extension _settings . sd . scale ,
width : extension _settings . sd . width ,
height : extension _settings . sd . height ,
2023-07-22 23:57:48 +03:00
prompt _prefix : combinePrefixes ( extension _settings . sd . prompt _prefix , getCharacterPrefix ( ) ) ,
2023-07-20 20:32:15 +03:00
negative _prompt : extension _settings . sd . negative _prompt ,
model : extension _settings . sd . model ,
nsfw : extension _settings . sd . horde _nsfw ,
restore _faces : ! ! extension _settings . sd . restore _faces ,
enable _hr : ! ! extension _settings . sd . enable _hr ,
} ) ,
} ) ;
if ( result . ok ) {
const data = await result . text ( ) ;
const base64Image = ` data:image/webp;base64, ${ data } ` ;
callback ? callback ( prompt , base64Image ) : sendMessage ( prompt , base64Image ) ;
} else {
toastr . error ( 'Image generation has failed. Please try again.' ) ;
}
}
async function sendMessage ( prompt , image ) {
const context = getContext ( ) ;
const messageText = ` [ ${ context . name2 } sends a picture that contains: ${ prompt } ] ` ;
const message = {
name : context . groupId ? systemUserName : context . name2 ,
is _system : context . groupId ? true : false ,
is _user : false ,
is _name : true ,
send _date : timestampToMoment ( Date . now ( ) ) . format ( 'LL LT' ) ,
mes : context . groupId ? p ( messageText ) : messageText ,
extra : {
image : image ,
title : prompt ,
} ,
} ;
context . chat . push ( message ) ;
context . addOneMessage ( message ) ;
context . saveChat ( ) ;
}
function addSDGenButtons ( ) {
const buttonHtml = `
< div id = "sd_gen" class = "list-group-item flex-container flexGap5" >
< div class = "fa-solid fa-paintbrush extensionsMenuExtensionButton" title = "Trigger Stable Diffusion" / > < / d i v >
Stable Diffusion
< / d i v >
` ;
const waitButtonHtml = `
< div id = "sd_gen_wait" class = "fa-solid fa-hourglass-half" / > < / d i v >
`
const dropdownHtml = `
< div id = "sd_dropdown" >
< ul class = "list-group" >
< span > Send me a picture of : < / s p a n >
< li class = "list-group-item" id = "sd_you" data - value = "you" > Yourself < / l i >
< li class = "list-group-item" id = "sd_face" data - value = "face" > Your Face < / l i >
< li class = "list-group-item" id = "sd_me" data - value = "me" > Me < / l i >
< li class = "list-group-item" id = "sd_world" data - value = "world" > The Whole Story < / l i >
< li class = "list-group-item" id = "sd_last" data - value = "last" > The Last Message < / l i >
< li class = "list-group-item" id = "sd_raw_last" data - value = "raw_last" > Raw Last Message < / l i >
< / u l >
< / d i v > ` ;
$ ( '#extensionsMenu' ) . prepend ( buttonHtml ) ;
$ ( '#extensionsMenu' ) . prepend ( waitButtonHtml ) ;
$ ( document . body ) . append ( dropdownHtml ) ;
const messageButton = $ ( '.sd_message_gen' ) ;
const button = $ ( '#sd_gen' ) ;
const waitButton = $ ( "#sd_gen_wait" ) ;
const dropdown = $ ( '#sd_dropdown' ) ;
waitButton . hide ( ) ;
dropdown . hide ( ) ;
button . hide ( ) ;
messageButton . hide ( ) ;
let popper = Popper . createPopper ( button . get ( 0 ) , dropdown . get ( 0 ) , {
placement : 'top' ,
} ) ;
$ ( document ) . on ( 'click' , '.sd_message_gen' , sdMessageButton ) ;
$ ( document ) . on ( 'click touchend' , function ( e ) {
const target = $ ( e . target ) ;
if ( target . is ( dropdown ) ) return ;
if ( target . is ( button ) && ! dropdown . is ( ":visible" ) && $ ( "#send_but" ) . css ( 'display' ) === 'flex' ) {
e . preventDefault ( ) ;
dropdown . fadeIn ( 250 ) ;
popper . update ( ) ;
} else {
dropdown . fadeOut ( 250 ) ;
}
} ) ;
}
function isConnectedToExtras ( ) {
return modules . includes ( 'sd' ) ;
}
async function moduleWorker ( ) {
if ( isConnectedToExtras ( ) || extension _settings . sd . horde ) {
$ ( '#sd_gen' ) . show ( ) ;
$ ( '.sd_message_gen' ) . show ( ) ;
}
else {
$ ( '#sd_gen' ) . hide ( ) ;
$ ( '.sd_message_gen' ) . hide ( ) ;
}
}
addSDGenButtons ( ) ;
setInterval ( moduleWorker , UPDATE _INTERVAL ) ;
async function sdMessageButton ( e ) {
function setBusyIcon ( isBusy ) {
$icon . toggleClass ( 'fa-paintbrush' , ! isBusy ) ;
$icon . toggleClass ( busyClass , isBusy ) ;
}
const busyClass = 'fa-hourglass' ;
const context = getContext ( ) ;
const $icon = $ ( e . currentTarget ) ;
const $mes = $icon . closest ( '.mes' ) ;
const message _id = $mes . attr ( 'mesid' ) ;
const message = context . chat [ message _id ] ;
const characterName = message ? . name || context . name2 ;
const messageText = substituteParams ( message ? . mes ) ;
const hasSavedImage = message ? . extra ? . image && message ? . extra ? . title ;
if ( $icon . hasClass ( busyClass ) ) {
console . log ( 'Previous image is still being generated...' ) ;
return ;
}
try {
setBusyIcon ( true ) ;
if ( hasSavedImage ) {
2023-07-22 20:32:58 +03:00
const prompt = await refinePrompt ( message . extra . title ) ;
message . extra . title = prompt ;
2023-07-20 20:32:15 +03:00
console . log ( 'Regenerating an image, using existing prompt:' , prompt ) ;
await sendGenerationRequest ( prompt , saveGeneratedImage ) ;
}
else {
console . log ( "doing /sd raw last" ) ;
await generatePicture ( 'sd' , 'raw_last' , ` ${ characterName } said: ${ messageText } ` , saveGeneratedImage ) ;
}
}
catch ( error ) {
console . error ( 'Could not generate inline image: ' , error ) ;
}
finally {
setBusyIcon ( false ) ;
}
function saveGeneratedImage ( prompt , image ) {
// Some message sources may not create the extra object
if ( typeof message . extra !== 'object' ) {
message . extra = { } ;
}
// If already contains an image and it's not inline - leave it as is
message . extra . inline _image = message . extra . image && ! message . extra . inline _image ? false : true ;
message . extra . image = image ;
message . extra . title = prompt ;
appendImageToMessage ( message , $mes ) ;
context . saveChat ( ) ;
}
} ;
$ ( "#sd_dropdown [id]" ) . on ( "click" , function ( ) {
var id = $ ( this ) . attr ( "id" ) ;
if ( id == "sd_you" ) {
console . log ( "doing /sd you" ) ;
generatePicture ( 'sd' , 'you' ) ;
}
else if ( id == "sd_face" ) {
console . log ( "doing /sd face" ) ;
generatePicture ( 'sd' , 'face' ) ;
}
else if ( id == "sd_me" ) {
console . log ( "doing /sd me" ) ;
generatePicture ( 'sd' , 'me' ) ;
}
else if ( id == "sd_world" ) {
console . log ( "doing /sd scene" ) ;
generatePicture ( 'sd' , 'scene' ) ;
}
else if ( id == "sd_last" ) {
console . log ( "doing /sd last" ) ;
generatePicture ( 'sd' , 'last' ) ;
}
else if ( id == "sd_raw_last" ) {
console . log ( "doing /sd raw last" ) ;
generatePicture ( 'sd' , 'raw_last' ) ;
}
} ) ;
jQuery ( async ( ) => {
getContext ( ) . registerSlashCommand ( 'sd' , generatePicture , [ ] , helpString , true , true ) ;
const settingsHtml = `
< div class = "sd_settings" >
< div class = "inline-drawer" >
< div class = "inline-drawer-toggle inline-drawer-header" >
2023-07-22 23:57:48 +03:00
< b > Stable Diffusion < / b >
< div class = "inline-drawer-icon fa-solid fa-circle-chevron-down down" > < / d i v >
2023-07-20 20:32:15 +03:00
< / d i v >
2023-07-22 23:57:48 +03:00
< div class = "inline-drawer-content" >
< small > < i > Use slash commands or the bottom Paintbrush button to generate images . Type < span class = "monospace" > / h e l p < / s p a n > i n c h a t f o r m o r e d e t a i l s < / i > < / s m a l l >
< br >
< small > < i > Hint : Save an API key in Horde KoboldAI API settings to use it here . < / i > < / s m a l l >
< label for = "sd_refine_mode" class = "checkbox_label" title = "Allow to edit prompts manually before sending them to generation API" >
< input id = "sd_refine_mode" type = "checkbox" / >
Edit prompts before generation
2023-07-20 20:32:15 +03:00
< / l a b e l >
2023-07-22 23:57:48 +03:00
< div class = "flex-container flexGap5 marginTop10 margin-bot-10px" >
< label class = "checkbox_label" >
< input id = "sd_horde" type = "checkbox" / >
Use Stable Horde
< / l a b e l >
< label style = "margin-left:1em;" class = "checkbox_label" >
< input id = "sd_horde_nsfw" type = "checkbox" / >
Allow NSFW images from Horde
< / l a b e l >
< / d i v >
< label for = "sd_scale" > CFG Scale ( < span id = "sd_scale_value" > < / s p a n > ) < / l a b e l >
< input id = "sd_scale" type = "range" min = "${defaultSettings.scale_min}" max = "${defaultSettings.scale_max}" step = "${defaultSettings.scale_step}" value = "${defaultSettings.scale}" / >
< label for = "sd_steps" > Sampling steps ( < span id = "sd_steps_value" > < / s p a n > ) < / l a b e l >
< input id = "sd_steps" type = "range" min = "${defaultSettings.steps_min}" max = "${defaultSettings.steps_max}" step = "${defaultSettings.steps_step}" value = "${defaultSettings.steps}" / >
< label for = "sd_width" > Width ( < span id = "sd_width_value" > < / s p a n > ) < / l a b e l >
< input id = "sd_width" type = "range" max = "${defaultSettings.dimension_max}" min = "${defaultSettings.dimension_min}" step = "${defaultSettings.dimension_step}" value = "${defaultSettings.width}" / >
< label for = "sd_height" > Height ( < span id = "sd_height_value" > < / s p a n > ) < / l a b e l >
< input id = "sd_height" type = "range" max = "${defaultSettings.dimension_max}" min = "${defaultSettings.dimension_min}" step = "${defaultSettings.dimension_step}" value = "${defaultSettings.height}" / >
< div > < small > Only for Horde or remote Stable Diffusion Web UI : < / s m a l l > < / d i v >
< div class = "flex-container marginTop10 margin-bot-10px" >
< label class = "flex1 checkbox_label" >
< input id = "sd_restore_faces" type = "checkbox" / >
Restore Faces
< / l a b e l >
< label class = "flex1 checkbox_label" >
< input id = "sd_enable_hr" type = "checkbox" / >
Hires . Fix
< / l a b e l >
< / d i v >
< label for = "sd_model" > Stable Diffusion model < / l a b e l >
< select id = "sd_model" > < / s e l e c t >
< label for = "sd_sampler" > Sampling method < / l a b e l >
< select id = "sd_sampler" > < / s e l e c t >
< div class = "flex-container flexGap5 margin-bot-10px" >
< label class = "checkbox_label" >
< input id = "sd_horde_karras" type = "checkbox" / >
Karras ( only for Horde , not all samplers supported )
< / l a b e l >
< / d i v >
< label for = "sd_prompt_prefix" > Common prompt prefix < / l a b e l >
< textarea id = "sd_prompt_prefix" class = "text_pole textarea_compact" rows = "3" > < / t e x t a r e a >
< div id = "sd_character_prompt_block" >
< label for = "sd_character_prompt" > Character - specific prompt prefix < / l a b e l >
< small > Won ' t be used in groups . < / s m a l l >
< textarea id = "sd_character_prompt" class = "text_pole textarea_compact" rows = "3" placeholder = "Any characteristics that describe the currently selected character. Will be added after a common prefix. Example: female, green eyes, brown hair, pink shirt" > < / t e x t a r e a >
< / d i v >
< label for = "sd_negative_prompt" > Negative prompt < / l a b e l >
< textarea id = "sd_negative_prompt" class = "text_pole textarea_compact" rows = "3" > < / t e x t a r e a >
2023-07-20 20:32:15 +03:00
< / d i v >
< / d i v >
2023-07-22 21:12:23 +03:00
< div class = "inline-drawer" >
< div class = "inline-drawer-toggle inline-drawer-header" >
< b > SD Prompt Templates < / b >
< div class = "inline-drawer-icon fa-solid fa-circle-chevron-down down" > < / d i v >
< / d i v >
< div id = "sd_prompt_templates" class = "inline-drawer-content" >
< / d i v >
< / d i v >
2023-07-20 20:32:15 +03:00
< / d i v > ` ;
$ ( '#extensions_settings' ) . append ( settingsHtml ) ;
$ ( '#sd_scale' ) . on ( 'input' , onScaleInput ) ;
$ ( '#sd_steps' ) . on ( 'input' , onStepsInput ) ;
$ ( '#sd_model' ) . on ( 'change' , onModelChange ) ;
$ ( '#sd_sampler' ) . on ( 'change' , onSamplerChange ) ;
$ ( '#sd_prompt_prefix' ) . on ( 'input' , onPromptPrefixInput ) ;
$ ( '#sd_negative_prompt' ) . on ( 'input' , onNegativePromptInput ) ;
$ ( '#sd_width' ) . on ( 'input' , onWidthInput ) ;
$ ( '#sd_height' ) . on ( 'input' , onHeightInput ) ;
$ ( '#sd_horde' ) . on ( 'input' , onHordeInput ) ;
$ ( '#sd_horde_nsfw' ) . on ( 'input' , onHordeNsfwInput ) ;
$ ( '#sd_horde_karras' ) . on ( 'input' , onHordeKarrasInput ) ;
$ ( '#sd_restore_faces' ) . on ( 'input' , onRestoreFacesInput ) ;
$ ( '#sd_enable_hr' ) . on ( 'input' , onHighResFixInput ) ;
$ ( '#sd_refine_mode' ) . on ( 'input' , onRefineModeInput ) ;
2023-07-22 23:57:48 +03:00
$ ( '#sd_character_prompt' ) . on ( 'input' , onCharacterPromptInput ) ;
$ ( '#sd_character_prompt_block' ) . hide ( ) ;
2023-07-20 20:32:15 +03:00
$ ( '.sd_settings .inline-drawer-toggle' ) . on ( 'click' , function ( ) {
initScrollHeight ( $ ( "#sd_prompt_prefix" ) ) ;
initScrollHeight ( $ ( "#sd_negative_prompt" ) ) ;
2023-07-23 00:28:23 +03:00
initScrollHeight ( $ ( "#sd_character_prompt" ) ) ;
2023-07-20 20:32:15 +03:00
} )
eventSource . on ( event _types . EXTRAS _CONNECTED , async ( ) => {
await Promise . all ( [ loadSamplers ( ) , loadModels ( ) ] ) ;
} ) ;
2023-07-22 23:57:48 +03:00
eventSource . on ( event _types . CHAT _CHANGED , onChatChanged ) ;
2023-07-20 20:32:15 +03:00
await loadSettings ( ) ;
$ ( 'body' ) . addClass ( 'sd' ) ;
} ) ;