622 lines
22 KiB
JavaScript
622 lines
22 KiB
JavaScript
const general = require('../general')
|
|
const psapi = require('../../psapi')
|
|
const html_manip = require('../html_manip')
|
|
function getDummyBase64() {
|
|
const b64Image =
|
|
'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAADMElEQVR4nOzVwQnAIBQFQYXff81RUkQCOyDj1YOPnbXWPmeTRef+/3O/OyBjzh3CD95BfqICMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMO0TAAD//2Anhf4QtqobAAAAAElFTkSuQmCC'
|
|
return b64Image
|
|
}
|
|
class hordeGenerator {
|
|
//horde generation process:
|
|
//*) get the settings
|
|
//*) get send request
|
|
//*) wait for response
|
|
//*) load the image to the canvas
|
|
//*) move and scale image to the selection
|
|
//*) save the image to history/data folder
|
|
//*) load the image data into the plugin / viewer tab
|
|
//*)
|
|
|
|
//other options:
|
|
//*)interrupt the generation process
|
|
//*)cancel the generation process on error
|
|
|
|
constructor() {
|
|
this.horde_settings
|
|
this.plugin_settings
|
|
this.currentGenerationResult = null
|
|
this.requestStatus = null
|
|
this.isProcessHordeResultCalled = false
|
|
this.maxWaitTime = 0
|
|
this.waiting = 0
|
|
this.isCanceled = false
|
|
}
|
|
|
|
async getSettings() {
|
|
const workers = await getWorkers()
|
|
|
|
const workers_ids = getWorkerID(workers)
|
|
const settings = await getSettings()
|
|
this.plugin_settings = settings
|
|
let payload = mapPluginSettingsToHorde(settings)
|
|
payload['workers'] = workers_ids
|
|
|
|
this.horde_settings = payload
|
|
return this.horde_settings
|
|
}
|
|
|
|
async generateRequest(settings) {
|
|
try {
|
|
g_id = null //reset request_id
|
|
this.requestStatus = await requestHorde(settings)
|
|
g_id = this.requestStatus.id
|
|
console.log(
|
|
'generateRequest this.requestStatus: ',
|
|
this.requestStatus
|
|
)
|
|
|
|
await this.startCheckingProgress()
|
|
} catch (e) {
|
|
g_id = null
|
|
console.warn(e)
|
|
}
|
|
}
|
|
async generate() {
|
|
//*) get the settings
|
|
this.horde_settings = await this.getSettings()
|
|
//*) send generateRequest() and trigger the progress bar update
|
|
this.isCanceled = false
|
|
await this.generateRequest(this.horde_settings)
|
|
//*) store the generation result in the currentGenerationResult
|
|
|
|
//*) return the generation currentGenerationResult
|
|
}
|
|
|
|
isValidGeneration() {
|
|
if (this.currentGenerationResult) {
|
|
return true // if true if valid, false otherwise
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
preGenerate() {}
|
|
// async layerToBase64WebpToFile
|
|
//convert layer to .webp file
|
|
//read the .webp file as buffer data base64 .webp
|
|
async layerToBase64Webp(layer, document_name, image_name) {
|
|
const width = html_manip.getWidth()
|
|
const height = html_manip.getHeight()
|
|
const image_buffer = await psapi.newExportPng(
|
|
layer,
|
|
image_name,
|
|
width,
|
|
height
|
|
)
|
|
|
|
const base64_image = _arrayBufferToBase64(image_buffer) //convert the buffer to base64
|
|
//send the base64 to the server to save the file in the desired directory
|
|
// await sdapi.requestSavePng(base64_image, image_name)
|
|
await saveFileInSubFolder(base64_image, document_name, image_name)
|
|
return base64_image
|
|
}
|
|
|
|
async layerToBase64ToFile(layer, document_name, image_name) {
|
|
const width = html_manip.getWidth()
|
|
const height = html_manip.getHeight()
|
|
const image_buffer = await psapi.newExportPng(
|
|
layer,
|
|
image_name,
|
|
width,
|
|
height
|
|
)
|
|
|
|
const base64_image = _arrayBufferToBase64(image_buffer) //convert the buffer to base64
|
|
//send the base64 to the server to save the file in the desired directory
|
|
// await sdapi.requestSavePng(base64_image, image_name)
|
|
await saveFileInSubFolder(base64_image, document_name, image_name)
|
|
return base64_image
|
|
}
|
|
async toSession(images_info) {
|
|
try {
|
|
//images_info[0] = {path:path,base64:base64png}
|
|
// let last_images_paths = await silentImagesToLayersExe(images_info)
|
|
let last_images_paths = {}
|
|
for (const image_info of images_info) {
|
|
const path = image_info['path']
|
|
// const base64_image = image_info['base64']
|
|
const layer = image_info['layer']
|
|
const [document_name, image_name] = path.split('/')
|
|
|
|
// await saveFileInSubFolder(base64_image, document_name, image_name)
|
|
image_info['base64'] = await this.layerToBase64ToFile(
|
|
layer,
|
|
document_name,
|
|
image_name
|
|
)
|
|
const json_file_name = `${image_name.split('.')[0]}.json`
|
|
this.plugin_settings['auto_metadata'] =
|
|
image_info?.auto_metadata
|
|
|
|
g_generation_session.base64OutputImages[path] =
|
|
image_info['base64']
|
|
await saveJsonFileInSubFolder(
|
|
this.plugin_settings,
|
|
document_name,
|
|
json_file_name
|
|
) //save the settings
|
|
last_images_paths[path] = image_info['layer']
|
|
}
|
|
|
|
if (g_generation_session.isFirstGeneration) {
|
|
//store them in the generation session for viewer manager to use
|
|
g_generation_session.image_paths_to_layers = last_images_paths
|
|
} else {
|
|
g_generation_session.image_paths_to_layers = {
|
|
...g_generation_session.image_paths_to_layers,
|
|
...last_images_paths,
|
|
}
|
|
// g_number_generation_per_session++
|
|
}
|
|
} catch (e) {
|
|
console.warn(e)
|
|
}
|
|
}
|
|
|
|
async interruptRequest() {
|
|
try {
|
|
console.log('interruptRquest():')
|
|
|
|
const full_url = `https://stablehorde.net/api/v2/generate/status/${g_id}`
|
|
|
|
console.log(full_url)
|
|
|
|
let response = await fetch(full_url, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'Content-Type': 'application/json',
|
|
apikey: '0000000000',
|
|
// 'Client-Agent': '4c79ab19-8e6c-4054-83b3-773b7ce71ece',
|
|
'Client-Agent': 'unknown:0:unknown',
|
|
},
|
|
})
|
|
|
|
let result = await response.json()
|
|
console.log('interruptReqquest result:', result)
|
|
|
|
return result
|
|
} catch (e) {
|
|
console.warn(e)
|
|
return
|
|
}
|
|
}
|
|
async interrupt() {
|
|
try {
|
|
html_manip.updateProgressBarsHtml(0)
|
|
this.isCanceled = true
|
|
g_interval_id = clearInterval(g_interval_id)
|
|
await this.interruptRequest()
|
|
} catch (e) {
|
|
console.warn(e)
|
|
}
|
|
}
|
|
async postGeneration() {
|
|
toggleTwoButtonsByClass(false, 'btnGenerateClass', 'btnInterruptClass')
|
|
}
|
|
async processHordeResult() {
|
|
//*) get the result from the horde server
|
|
//*) save them locally to output directory
|
|
//*) import them into the canvas
|
|
//*) resize and move the layers to fit the selection
|
|
//*) return the results to be stored and processed by the g_generation_session
|
|
try {
|
|
if (this.isProcessHordeResultCalled) {
|
|
return
|
|
}
|
|
this.isProcessHordeResultCalled = true
|
|
console.log('horde request is done')
|
|
// g_b_request_result = true
|
|
const temp_id = g_id //g_id will reset
|
|
// cancelRequestClientSide()
|
|
g_horde_generation_result = await requestHordeStatus(temp_id)
|
|
|
|
const generations = g_horde_generation_result.generations
|
|
const writeable_entry = await getCurrentDocFolder()
|
|
const images_info = [] //{path:image_path,base64:}
|
|
for (const image_horde_container of generations) {
|
|
try {
|
|
const url = image_horde_container.img
|
|
const image_file_name = general.newOutputImageName('webp')
|
|
|
|
const image_layer = await downloadItExe(
|
|
url,
|
|
writeable_entry,
|
|
image_file_name
|
|
) //download the image from url, it works even with .webp format
|
|
const image_png_file_name =
|
|
general.convertImageNameToPng(image_file_name)
|
|
|
|
const uuid = await getUniqueDocumentId()
|
|
const image_path = `${uuid}/${image_png_file_name}` //this is the png path
|
|
images_info.push({
|
|
path: image_path,
|
|
base64: getDummyBase64(),
|
|
layer: image_layer,
|
|
})
|
|
await psapi.layerToSelection(
|
|
g_generation_session.selectionInfo
|
|
) //TODO: create a safe layerToSelection function
|
|
} catch (e) {
|
|
console.warn(e)
|
|
}
|
|
}
|
|
this.isProcessHordeResultCalled = false //reset for next generation
|
|
return images_info
|
|
} catch (e) {
|
|
console.warn(e)
|
|
}
|
|
}
|
|
async startCheckingProgress() {
|
|
if (!g_interval_id && g_id) {
|
|
g_interval_id = setInterval(async () => {
|
|
try {
|
|
if (this.isCanceled) {
|
|
html_manip.updateProgressBarsHtml(0)
|
|
return
|
|
}
|
|
const check_json = await requestHordeCheck(g_id)
|
|
|
|
//update the progress bar proceduer
|
|
console.log('this.maxWaitTime: ', this.maxWaitTime)
|
|
console.log(
|
|
"check_json['wait_time']: ",
|
|
check_json['wait_time']
|
|
)
|
|
console.log(
|
|
"check_json['waiting']: ",
|
|
check_json['waiting']
|
|
)
|
|
|
|
this.maxWaitTime = Math.max(
|
|
check_json['wait_time'],
|
|
this.maxWaitTime
|
|
) // return the max time value, so we could use to calculate the complection percentage
|
|
const delta_time =
|
|
this.maxWaitTime - check_json['wait_time']
|
|
|
|
if (
|
|
isNaN(this.maxWaitTime) ||
|
|
parseInt(this.maxWaitTime) === 0
|
|
) {
|
|
this.maxWaitTime = 0 // reset to zero
|
|
} else {
|
|
const completion_percentage =
|
|
(delta_time / this.maxWaitTime) * 100
|
|
|
|
html_manip.updateProgressBarsHtml(completion_percentage)
|
|
}
|
|
|
|
//
|
|
|
|
if (
|
|
check_json['done'] &&
|
|
g_interval_id
|
|
// !g_b_request_result
|
|
) {
|
|
g_interval_id = clearInterval(g_interval_id)
|
|
|
|
const images_info = await this.processHordeResult()
|
|
|
|
await this.toSession(images_info)
|
|
html_manip.updateProgressBarsHtml(0) // reset progress bar
|
|
|
|
await psapi.reSelectMarqueeExe(
|
|
g_generation_session.selectionInfo
|
|
)
|
|
//update the viewer
|
|
await this.postGeneration()
|
|
await loadViewerImages()
|
|
}
|
|
} catch (e) {
|
|
console.warn(e)
|
|
}
|
|
}, 3000)
|
|
}
|
|
}
|
|
}
|
|
const webui_to_horde_samplers = {
|
|
'Euler a': 'k_euler_a',
|
|
Euler: 'k_euler',
|
|
LMS: 'k_lms',
|
|
Heun: 'k_heun',
|
|
DPM2: 'k_dpm_2',
|
|
'DPM2 a': 'k_dpm_2_a',
|
|
'DPM++ 2S a': 'k_dpmpp_2s_a',
|
|
'DPM++ 2M': 'k_dpmpp_2m',
|
|
'DPM++ SDE': 'k_dpmpp_sde',
|
|
'DPM fast': 'k_dpm_fast',
|
|
'DPM adaptive': 'k_dpm_adaptive',
|
|
'LMS Karras': 'k_lms',
|
|
'DPM2 Karras': 'k_dpm_2',
|
|
'DPM2 a Karras': 'k_dpm_2_a',
|
|
'DPM++ 2S a Karras': 'k_dpmpp_2s_a',
|
|
'DPM++ 2M Karras': 'k_dpmpp_2m',
|
|
'DPM++ SDE Karras': 'k_dpmpp_sde',
|
|
DDIM: 'ddim',
|
|
PLMS: 'plms',
|
|
}
|
|
|
|
//get workers
|
|
//select a worker
|
|
//send a request => requestHorde(horde_settings)
|
|
//check for progress => requestHordeCheck(request_id)
|
|
//when progress is full, request the result => requestHordeStatus(request_id)
|
|
|
|
function mapPluginSettingsToHorde(plugin_settings) {
|
|
const { getModelHorde } = require('../sd_scripts/horde')
|
|
const ps = plugin_settings // for shortness
|
|
const sampler = webui_to_horde_samplers[ps['sampler_index']]
|
|
const model = getModelHorde()
|
|
let horde_prompt
|
|
if (ps['negative_prompt'].length > 0) {
|
|
horde_prompt = `${ps['prompt']} ### ${ps['negative_prompt']}`
|
|
} else {
|
|
horde_prompt = ps['prompt'] //no negative prompt
|
|
}
|
|
|
|
if (ps['mode'] === 'img2img') {
|
|
payload['source_image'] = ps['init_images']
|
|
// payload['source_image'] = base64.b64encode(buffer.getvalue()).decode() //does it need to be webp?
|
|
payload['source_processing'] = 'img2img'
|
|
} else if (ps['mode'] === 'inpaint') {
|
|
payload['source_processing'] = 'inpainting'
|
|
|
|
// payload["source_mask"] = base64.b64encode(buffer.getvalue()).decode()//does it need to be webp?
|
|
}
|
|
|
|
let horde_payload = {
|
|
prompt: horde_prompt,
|
|
params: {
|
|
sampler_name: sampler,
|
|
toggles: [1, 4],
|
|
cfg_scale: ps['cfg_scale'],
|
|
denoising_strength: ps['denoising_strength'],
|
|
// seed: 'string',
|
|
height: ps['height'],
|
|
width: ps['width'],
|
|
seed_variation: 1,
|
|
post_processing: ['GFPGAN'],
|
|
karras: false,
|
|
tiling: false,
|
|
steps: parseInt(ps['steps']),
|
|
n: 1,
|
|
},
|
|
nsfw: false,
|
|
trusted_workers: true,
|
|
censor_nsfw: false,
|
|
// workers: ['4c79ab19-8e6c-4054-83b3-773b7ce71ece'],
|
|
// workers: workers_ids,
|
|
// models: ['stable_diffusion'],
|
|
models: [model],
|
|
// source_image: 'string',
|
|
// source_processing: 'img2img',
|
|
// source_mask: 'string',
|
|
r2: true,
|
|
shared: false,
|
|
}
|
|
return horde_payload
|
|
}
|
|
|
|
function getWorkerID(workers_json) {
|
|
let workers_ids = []
|
|
for (worker of workers_json) {
|
|
workers_ids.push(worker?.id)
|
|
}
|
|
console.log('workers_ids:', workers_ids)
|
|
|
|
return workers_ids
|
|
}
|
|
async function getWorkers() {
|
|
const full_url = 'https://stablehorde.net/api/v2/workers'
|
|
// const full_url = 'https://stablehorde.net/api/v2/generate/sync'
|
|
console.log(full_url)
|
|
|
|
let request = await fetch(full_url, {
|
|
method: 'GET',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
})
|
|
|
|
let workers = await request.json()
|
|
// const workers_ids = getWorkerID(workers)
|
|
console.log('requestHorde workers:', workers)
|
|
return workers
|
|
}
|
|
async function requestHorde(payload) {
|
|
// const workers = await getWorkers()
|
|
|
|
// const workers_ids = getWorkerID(workers)
|
|
// const settings = await getSettings()
|
|
// payload = mapPluginSettingsToHorde(settings)
|
|
// payload['workers'] = workers_ids
|
|
// payload = {
|
|
// prompt: 'string',
|
|
// params: {
|
|
// sampler_name: 'k_lms',
|
|
// toggles: [1, 4],
|
|
// cfg_scale: 5,
|
|
// denoising_strength: 0.75,
|
|
// // seed: 'string',
|
|
// height: 512,
|
|
// width: 512,
|
|
// seed_variation: 1,
|
|
// post_processing: ['GFPGAN'],
|
|
// karras: false,
|
|
// tiling: false,
|
|
// steps: 5,
|
|
// n: 1,
|
|
// },
|
|
// nsfw: false,
|
|
// trusted_workers: true,
|
|
// censor_nsfw: false,
|
|
// // workers: ['4c79ab19-8e6c-4054-83b3-773b7ce71ece'],
|
|
// workers: workers_ids,
|
|
// models: ['stable_diffusion'],
|
|
// // source_image: 'string',
|
|
// // source_processing: 'img2img',
|
|
// // source_mask: 'string',
|
|
// r2: true,
|
|
// shared: false,
|
|
// }
|
|
try {
|
|
console.log('requestHorde():')
|
|
|
|
const full_url = 'https://stablehorde.net/api/v2/generate/async'
|
|
// const full_url = 'https://stablehorde.net/api/v2/generate/sync'
|
|
console.log(full_url)
|
|
|
|
let request = await fetch(full_url, {
|
|
method: 'POST',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'Content-Type': 'application/json',
|
|
apikey: '0000000000',
|
|
// 'Client-Agent': '4c79ab19-8e6c-4054-83b3-773b7ce71ece',
|
|
'Client-Agent': 'unknown:0:unknown',
|
|
},
|
|
body: JSON.stringify(payload),
|
|
})
|
|
|
|
let json = await request.json()
|
|
console.log('requestHorde json:', json)
|
|
|
|
return json
|
|
} catch (e) {
|
|
console.warn(e)
|
|
return {}
|
|
}
|
|
}
|
|
async function requestHordeCheck(id) {
|
|
try {
|
|
console.log('requestHordeCheck():')
|
|
const base_url = 'https://stablehorde.net/api/v2/generate/check'
|
|
|
|
const full_url = `${base_url}/${id}`
|
|
// const full_url = 'https://stablehorde.net/api/v2/generate/sync'
|
|
console.log(full_url)
|
|
const payload = {}
|
|
let request = await fetch(full_url, {
|
|
method: 'GET',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'Content-Type': 'application/json',
|
|
// 'Client-Agent': '4c79ab19-8e6c-4054-83b3-773b7ce71ece',
|
|
'Client-Agent': 'unknown:0:unknown',
|
|
},
|
|
})
|
|
|
|
let json = await request.json()
|
|
console.log('requestHordeCheck json:', json)
|
|
|
|
return json
|
|
} catch (e) {
|
|
console.warn(e)
|
|
return {}
|
|
}
|
|
}
|
|
|
|
async function requestHordeStatus(id) {
|
|
try {
|
|
console.log('requestHordeStatus():')
|
|
const base_url = 'https://stablehorde.net/api/v2/generate/status'
|
|
|
|
const full_url = `${base_url}/${id}`
|
|
// const full_url = 'https://stablehorde.net/api/v2/generate/sync'
|
|
console.log(full_url)
|
|
const payload = {}
|
|
let request = await fetch(full_url, {
|
|
method: 'GET',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'Content-Type': 'application/json',
|
|
// 'Client-Agent': '4c79ab19-8e6c-4054-83b3-773b7ce71ece',
|
|
'Client-Agent': 'unknown:0:unknown',
|
|
},
|
|
})
|
|
|
|
let json = await request.json()
|
|
console.log('requestHordeStatus json:', json)
|
|
|
|
return json
|
|
} catch (e) {
|
|
console.warn(e)
|
|
}
|
|
}
|
|
|
|
let g_interval_id
|
|
let g_id
|
|
let g_horde_generation_result
|
|
let g_b_request_result = false
|
|
function cancelRequestClientSide() {
|
|
g_interval_id = clearInterval(g_interval_id)
|
|
g_id = null
|
|
g_b_request_result = false
|
|
}
|
|
|
|
// async function processHordeResult() {
|
|
// try {
|
|
// const check_json = await requestHordeCheck(g_id)
|
|
// if (
|
|
// check_json['done'] &&
|
|
// g_interval_id
|
|
// // !g_b_request_result
|
|
// ) {
|
|
// clearInterval(g_interval_id)
|
|
// console.log('horde request is done')
|
|
// // g_b_request_result = true
|
|
// const temp_id = g_id //g_id will reset
|
|
// // cancelRequestClientSide()
|
|
// g_horde_generation_result = await requestHordeStatus(temp_id)
|
|
|
|
// const generations = g_horde_generation_result.generations
|
|
// const writeable_entry = await getCurrentDocFolder()
|
|
// for (image_horde_container of generations) {
|
|
// try {
|
|
// const url = image_horde_container.img
|
|
// image_file_name = general.newOutputImageName('webp')
|
|
// const image_layer = await downloadItExe(
|
|
// url,
|
|
// writeable_entry,
|
|
// image_file_name
|
|
// ) //
|
|
|
|
// await psapi.layerToSelection(
|
|
// g_generation_session.selectionInfo
|
|
// ) //TODO: create a safe layerToSelection function
|
|
// } catch (e) {
|
|
// console.warn(e)
|
|
// }
|
|
// }
|
|
// }
|
|
// } catch (e) {
|
|
// console.warn(e)
|
|
// }
|
|
// }
|
|
// async function startCheckingProgress() {
|
|
// if (!g_interval_id && g_id) {
|
|
// g_interval_id = setInterval(async () => {
|
|
// await processHordeResult()
|
|
// }, 3000)
|
|
// }
|
|
// }
|
|
|
|
module.exports = {
|
|
// requestHorde,
|
|
// requestHordeCheck,
|
|
// requestHordeStatus,
|
|
// requestHordeMain,
|
|
// getWorkers,
|
|
hordeGenerator,
|
|
}
|