Browse Source

Improvements and fixes for queue.

tags/v1.4.4
Ivan Bravo Bravo 9 months ago
parent
commit
544b85f9f5

+ 0
- 1
.github/workflows/ci.yml View File

@@ -3,7 +3,6 @@ name: CI/CD
on:
push:
branches:
- canary
- feature/*
- hotfix/*
tags:

+ 1
- 1
src/.env-cmdrc.js View File

@@ -1,7 +1,7 @@
module.exports = {
"default": {
"SERVER_HOST": "localhost",
"SERVER_PORT": 3000,
"SERVER_PORT": 4000,
"NUCLEUS_APPID": "5d353cecbe5ccc0133cf90f4"
},
"development": {

BIN
src/assets/images/background.png View File


BIN
src/assets/images/curls.png View File


+ 6
- 6
src/components/Layout/Queuebar.vue View File

@@ -9,16 +9,16 @@

<div v-show="$nudify.waiting.length > 0" class="section__actions">
<button
v-tooltip="{placement: 'bottom', content: 'Forget all'}"
v-tooltip="{placement: 'bottom', content: 'Forget waiting'}"
class="button button--danger button--xs"
@click.prevent="$nudify.forgetAll('waiting')">
<font-awesome-icon icon="trash-alt" />
</button>

<button
v-tooltip="{placement: 'bottom', content: 'Stop all' }"
v-tooltip="{placement: 'bottom', content: 'Cancel waiting' }"
class="button button--xs"
@click.prevent="$nudify.stopAll('waiting')">
@click.prevent="$nudify.cancelAll('waiting')">
<font-awesome-icon icon="stop" />
</button>
</div>
@@ -54,7 +54,7 @@
<button
v-tooltip="'Start all'"
class="button button--xs"
@click.prevent="$nudify.startAll()">
@click.prevent="$nudify.addAll()">
<font-awesome-icon icon="play" />
</button>
</div>
@@ -87,9 +87,9 @@
</button>

<button
v-tooltip="'Restart all'"
v-tooltip="'Rerun all'"
class="button button--xs"
@click.prevent="$nudify.startAll('finished')">
@click.prevent="$nudify.addAll('finished')">
<font-awesome-icon icon="undo" />
</button>
</div>

+ 16
- 6
src/components/Layout/Topbar.vue View File

@@ -16,15 +16,15 @@
</div>

<div class="topbar__buttons">
<button type="button" @click="minimize">
<button id="minimize" type="button" @click="minimize">
<font-awesome-icon icon="minus" />
</button>

<button type="button" @click="maximize">
<button id="maximize" type="button" @click="maximize">
<font-awesome-icon :icon="['far', 'square']" />
</button>

<button type="button" class="close" @click="close">
<button id="close" type="button" class="close" @click="close">
<font-awesome-icon icon="times" />
</button>
</div>
@@ -34,7 +34,9 @@
<script>
import dayjs from 'dayjs'

const { activeWindow, api } = $provider.util
const { getCurrentWindow } = require('electron').remote

const { api } = $provider.util

export default {
data: () => ({
@@ -75,11 +77,19 @@ export default {

methods: {
minimize() {
activeWindow().minimize()
try {
getCurrentWindow().minimize()
} catch (error) {
throw new Exception('There was a problem trying to minimize the window...', error)
}
},

maximize() {
activeWindow().maximize()
try {
getCurrentWindow().maximize()
} catch (error) {
throw new Exception('There was a problem trying to maximize the window...', error)
}
},

close() {

+ 40
- 10
src/components/Nudity/PhotoRun.vue View File

@@ -1,5 +1,7 @@
<template>
<div class="c-photo-run" :style="previewStyle" data-private>
<div class="photo-run" :class="previewClass" data-private>
<div class="run__preview" :style="previewStyle" />

<div v-if="run.preferences.body.randomize || run.preferences.body.progressive.enabled" class="run__preferences">
<div class="preference">
<span>Boobs</span>
@@ -161,13 +163,21 @@ export default {

computed: {
previewStyle() {
if (!this.run.outputFile.exists) {
if (!this.run.finished) {
return {}
}

return { backgroundImage: `url(${this.run.outputFile.path})` }
},

previewClass() {
return {
'run--failed': this.run.failed,
'run--running': this.run.running,
'run--finished': this.run.finished,
}
},

hasMaskfin() {
return this.run.maskfinFile.exists
},
@@ -196,11 +206,11 @@ export default {
},

rerun() {
this.run.photo.rerun(this.run)
this.run.add()
},

cancel() {
this.run.photo.cancelRun(this.run)
this.run.cancel()
},

addMaskToQueue() {
@@ -228,11 +238,26 @@ export default {
</script>

<style lang="scss" scoped>
.c-photo-run {
@apply bg-cover bg-center border border-dark-500;
.photo-run {
@apply relative;
background-image: url('~@/assets/images/background.png');
@apply bg-cover bg-center border-2 border-dark-100;
background-image: url('~@/assets/images/curls.png'); /* Background pattern from Toptal Subtle Patterns */
min-height: 512px;
transition: all .15s linear;

&.run--failed {
@apply border-danger-500;
}

&.run--running {
@apply border-primary-500;
}

&.run--finished {
.run__preview {
@apply opacity-100;
}
}

&:hover {
.run__content,
@@ -240,14 +265,19 @@ export default {
@apply opacity-100;
}
}

.run__preview {
@apply absolute opacity-0 left-0 right-0 top-0 bottom-0 z-10;
transition: all .3s linear;
}
}

.run__content,
.run__preferences {
@apply absolute opacity-0 bg-dark-500-80 w-full;
@apply flex;
@apply absolute z-20;
@apply flex opacity-0 bg-dark-500-80 w-full;
backdrop-filter: blur(6px);
transition: all .1s linear;
transition: all .15s linear;
}

.run__preferences {

+ 8
- 4
src/modules/config/cli-errors.json View File

@@ -1,7 +1,7 @@
[
{
"query": "Found no NVIDIA driver on your system",
"message": "Found no NVIDIA driver on your system. Please check that you have an NVIDIA GPU and installed the latest driver."
"message": "Found no NVIDIA driver on your system. Please check that you have an NVIDIA GPU and the latest drivers."
},
{
"query": "The NVIDIA driver on your system is too old",
@@ -9,7 +9,7 @@
},
{
"query": "no longer supports this GPU",
"message": "DreamTime no longer supports your GPU."
"message": "The algorithm no longer supports your GPU."
},
{
"query": "Buy new RAM!",
@@ -29,7 +29,7 @@
},
{
"query": "Image is not 512 x 512",
"message": "It is not possible to transform photos other than 512x512, please resize the photo with any of the DreamTime options or manually before uploading."
"message": "Your photo needs to be 512x512 to be transformed, please resize the photo with any of the DreamTime options or manually before uploading."
},
{
"query": "loading Python",
@@ -49,6 +49,10 @@
},
{
"query": "not compiled with CUDA",
"message": "You have installed the CPU-only version of DreamPower. Please reinstall DreamPower to obtain GPU support."
"message": "You have installed the CPU-only version of DreamPower. Please reinstall DreamPower to get GPU support."
},
{
"query": "file not a supported format",
"message": "There was a problem loading the photo, please make sure it is not corrupt and is of a valid format."
}
]

+ 3
- 2
src/modules/file.js View File

@@ -111,9 +111,9 @@ export class File {
fs.unlinkSync(filepath)
consola.debug(`File deleted: ${filepath}`)
})
} else {
this.open(filepath)
}

this.open(filepath)
}
}

@@ -174,6 +174,7 @@ export class File {
* @param {string} data
*/
async writeDataURL(data) {
console.log('writeDataURL', this.path, this)
fs.writeDataURL(this.path, data)
await this.open()


+ 1
- 0
src/modules/nudify/index.js View File

@@ -8,4 +8,5 @@
// Written by Ivan Bravo Bravo <ivan@dreamnet.tech>, 2019.

export { Nudify } from './nudify'
export { NudifyQueue } from './queue'
export { NudifyStore } from './nudify-store'

+ 9
- 6
src/modules/nudify/nudify-store.js View File

@@ -7,6 +7,7 @@
//
// Written by Ivan Bravo Bravo <ivan@dreamnet.tech>, 2019.

import { debounce } from 'lodash'
import { Nudify } from './nudify'
import { events } from '../events'

@@ -24,13 +25,15 @@ const store = {
finished: [],

setup() {
events.on('nudify.update', () => {
this.photos = Nudify.photos
this.waiting = Nudify.waiting
this.pending = Nudify.pending
this.finished = Nudify.finished
})
events.on('nudify.update', this.update)
},

update: debounce(() => {
store.photos = Nudify.photos
store.waiting = Nudify.waiting
store.pending = Nudify.pending
store.finished = Nudify.finished
}, 300, { leading: true }),
}

export const NudifyStore = new Proxy(store, {

+ 75
- 128
src/modules/nudify/nudify.js View File

@@ -9,13 +9,14 @@

import {
startsWith, find, isNil,
filter, debounce,
filter,
remove, clone,
} from 'lodash'
import { basename } from 'path'
import Queue from 'better-queue'
import MemoryStore from 'better-queue-memory'
import delay from 'delay'
import Swal from 'sweetalert2/dist/sweetalert2.js'
import { watch } from 'melanke-watchjs'
import { NudifyQueue } from './queue'
import { events } from '../events'
import { Photo } from './photo'
import { File } from '../file'
@@ -45,106 +46,62 @@ const Toast = Swal.mixin({

/**
* Entry point for photo nudification.
* TODO: Refactor. Separate the entry points and the Queue.
*/
export class Nudify {
/**
* Queue. Photos that are waiting transformation.
* @type {Queue}
*/
static queue

export const Nudify = {
/**
* All open photos.
* @type {Array<Photo>}
*/
static photos = []

/**
* @type {Function}
*/
static emitUpdate = debounce(() => {
events.emit('nudify.update')
}, 300, { leading: true })
photos: [],

/**
* @type {Array<Photo>}
*/
static get waiting() {
get waiting() {
return filter(this.photos, (photo) => photo.status === 'waiting' || photo.status === 'running')
}
},

/**
* @type {Array<Photo>}
*/
static get pending() {
get pending() {
return filter(this.photos, { status: 'pending' })
}
},

/**
* @type {Array<Photo>}
*/
static get finished() {
get finished() {
return filter(this.photos, { status: 'finished' })
}

/**
*
*/
static setup() {
this.queue = new Queue(this._run, {
maxTimeout: (3 * 60 * 60 * 1000), // 3 hours.
afterProcessDelay: 1000,
batchSize: 1,
concurrent: 1,
store: new MemoryStore,
})

this.queue.on('task_queued', (photoId, photo) => {
photo.status = 'waiting'
})
}
},

/**
*
* @param {Photo} photo
* @param {Function} cb
*/
static _run(photo, cb) {
try {
photo.start().then(() => {
cb()
return true
}).catch((error) => {
cb(error)
})
} catch (error) {
cb(error)
}
setup() {
NudifyQueue.setup()

return {
cancel() {
photo.cancel('pending')
},
}
}
watch(this, 'photos', () => {
events.emit('nudify.update')
}, 1)
},

/**
*
* @param {string} id
*/
static getPhoto(id) {
getPhotoById(id) {
return find(this.photos, { id })
}
},

/**
* Add a new file to the Queue.
* Add a new file.
* @param {File} input
*/
static add(file, params = {}) {
add(file, params = {}) {
const photo = new Photo(file, params)

const exists = this.getPhoto(photo.id)
const exists = this.getPhotoById(photo.id)

if (!isNil(exists)) {
return
@@ -152,10 +109,6 @@ export class Nudify {

this.photos.unshift(photo)

consola.debug(`Photo added: ${file.fullname}`)

this.emitUpdate()

if (this.photos.length > MAX_PHOTOS) {
// Delete the oldest photo.
this.photos.pop()
@@ -167,26 +120,31 @@ export class Nudify {
const { uploadMode } = settings.app

if (uploadMode === 'add-queue') {
this.addToQueue(photo)
photo.add()
} else if (uploadMode === 'go-preferences') {
window.$redirect(`/nudify/${photo.id}/preferences`)
}
}
}
},

/**
*
* @param {string} filepath
* @param {string} path
*/
static async addFile(filepath) {
const filesMetadata = await getFilesMetadata(filepath)
const multiple = filesMetadata.length > 1
async addFile(path) {
const metadatas = await getFilesMetadata(path)
const multiple = metadatas.length > 1

filesMetadata.forEach((metadata) => {
metadatas.forEach((metadata) => {
const file = File.fromMetadata(metadata)

try {
this.add(file)

Toast.fire({
icon: 'success',
title: basename(path),
})
} catch (err) {
if (multiple) {
consola.warn('Error adding a photo.', err)
@@ -195,18 +153,13 @@ export class Nudify {
}
}
})

Toast.fire({
icon: 'success',
title: basename(filepath),
})
}
},

/**
*
* @param {string} paths
* @param {Array} paths
*/
static async addFiles(paths) {
async addFiles(paths) {
Swal.fire({
title: 'Importing files...',
text: 'One moment, please.',
@@ -217,17 +170,19 @@ export class Nudify {

Swal.showLoading()

await delay(1000)

for (const path of paths) {
// eslint-disable-next-line no-await-in-loop
await this.addFile(path)
}
}
},

/**
*
* @param {string} url
*/
static async addUrl(url) {
async addUrl(url) {
if (!startsWith(url, 'http://') && !startsWith(url, 'https://')) {
throw new Warning('Upload failed.', 'Please enter a valid web address.')
}
@@ -240,6 +195,8 @@ export class Nudify {
allowEscapeKey: false,
})

await delay(500)

try {
const file = await File.fromUrl(url)

@@ -249,68 +206,56 @@ export class Nudify {
} catch (error) {
throw new Warning('Upload failed.', 'Unable to download the photo, please verify that the address is correct and that you are connected to the Internet.', error)
}
}

/**
*
* @param {Photo} photo
*/
static addToQueue(photo) {
this.queue.push(photo)
}

/**
*
* @param {Photo} photo
*/
static removeFromQueue(photo) {
photo.cancel('pending')
this.queue.cancel(photo.id)
}

/**
*
* @param {Photo} photo
*/
static forget(photo) {
this.removeFromQueue(photo)

// eslint-disable-next-line lodash/prefer-immutable-method
remove(this.photos, { id: photo.id })
consola.debug(`Forgotten: ${photo.file.fullname}`)

this.emitUpdate()
}
},

/**
*
* @param {string} status
*/
static startAll(status = 'pending') {
addAll(status = 'pending') {
this.photos.forEach((photo) => {
if (photo.status !== status) {
return
}

this.addToQueue(photo)
photo.add()
})
}
},

/**
*
* @param {string} status
*/
static stopAll(status = 'pending') {
cancelAll(status = 'pending') {
this.photos.forEach((photo) => {
if (photo.status !== status) {
return
}

this.removeFromQueue(photo)
photo.cancel()
})
}
},

static async forgetAll(status = 'pending') {
/**
*
* @param {Photo} photo
*/
forget(photo) {
photo.cancel()

// eslint-disable-next-line lodash/prefer-immutable-method
remove(this.photos, { id: photo.id })

events.emit('nudify.update')

consola.debug(`Forgotten: ${photo.file.fullname}`)
},

/**
*
* @param {string} status
*/
async forgetAll(status = 'pending') {
const response = await Swal.fire({
title: 'Are you sure?',
text: 'Forgetting will remove all photos from the queue (it will not delete the files) and free up memory.',
@@ -324,6 +269,8 @@ export class Nudify {
return
}

window.$redirect('/')

const photosCopy = clone(this.photos)

photosCopy.forEach((photo) => {
@@ -331,7 +278,7 @@ export class Nudify {
return
}

this.forget(photo)
photo.forget()
})
}
},
}

+ 101
- 67
src/modules/nudify/photo-run.js View File

@@ -29,7 +29,7 @@ export class PhotoRun {
id

/**
* @type {require('./photo').Photo}
* @type {Photo}
*/
photo

@@ -76,20 +76,16 @@ export class PhotoRun {
error: '',
}

get running() {
return this.status === 'running'
}

get pending() {
return this.status === 'pending'
}

get finished() {
return this.status === 'finished'
get running() {
return this.status === 'running'
}

get started() {
return this.status !== 'pending'
get finished() {
return this.status === 'finished'
}

get outputName() {
@@ -118,20 +114,9 @@ export class PhotoRun {
this.preferences = cloneDeep(this.photo.preferences)
}

reset() {
this.cli = {
lines: [],
error: '',
}

this.preferences = cloneDeep(this.photo.preferences)

this.status = 'pending'
this.failed = false

this.timer = new Timer
}

/**
*
*/
toObject() {
return {
photo: {
@@ -145,10 +130,56 @@ export class PhotoRun {
}
}

beforeStart() {
this.setupPreferences()
/**
*
*/
setupPreferences() {
this.preferences = cloneDeep(this.photo.preferences)
const preferences = this.preferences.body

if (preferences.randomize) {
// randomize.
forIn(preferencesConfig, (payload, key) => {
const { enabled, min, max } = preferences[key].randomize

if (enabled) {
preferences[key].size = random(min, max, true)
}
})
} else if (preferences.progressive.enabled) {
// progressive.
const add = preferences.progressive.rate * (this.id - 1)

forIn(preferencesConfig, (payload, key) => {
if (preferences[key].progressive) {
let value = Number.parseFloat(preferences[key].size)
value = Math.min(value + add, payload.max)

preferences[key].size = value
}
})
}

this.preferences.body = preferences
}

/**
*
*/
add() {
this.photo.addRun(this)
}

/**
*
*/
cancel() {
this.photo.cancelRun(this)
}

/**
*
*/
start() {
const def = deferred()

@@ -156,7 +187,7 @@ export class PhotoRun {
def.reject(new Warning('Failed to start.', 'There was a problem trying to start DreamPower, make sure the installation is not corrupt.', error))
}

this.beforeStart()
// this.onStart()

try {
this.process = transform(this.toObject())
@@ -165,15 +196,15 @@ export class PhotoRun {
return def.promise
}

const { consola } = this.photo
// const { consola } = this.photo

this.process.on('error', (error) => {
// spawn error
// DreamPower could not start.
onSpawnError(error)
})

this.process.on('stdout', (output) => {
// cli output
// DreamPower Output.
output.forEach((text) => {
text = toString(text)

@@ -182,14 +213,14 @@ export class PhotoRun {
css: {},
})

consola.debug(text)
// consola.debug(text)
})
})

this.process.on('stderr', (output) => {
const text = toString(output)

// cli error
// DreamPower Errors.
this.cli.lines.unshift({
text,
css: {
@@ -199,7 +230,7 @@ export class PhotoRun {

this.cli.error += `${text}\n`

consola.debug(text)
// consola.debug(text)
})

this.process.on('success', async () => {
@@ -228,68 +259,68 @@ export class PhotoRun {
return def.promise
}

cancel() {
if (!this.running || isNil(this.process)) {
/**
*
*/
stop() {
if (isNil(this.process)) {
return
}

this.process.emit('cancel')
}

setupPreferences() {
const preferences = this.preferences.body

if (preferences.randomize) {
// randomize.
forIn(preferencesConfig, (payload, key) => {
const { enabled, min, max } = preferences[key].randomize

if (enabled) {
preferences[key].size = random(min, max, true)
}
})
} else if (preferences.progressive.enabled) {
// progressive.
const add = preferences.progressive.rate * (this.id - 1)

forIn(preferencesConfig, (payload, key) => {
if (preferences[key].progressive) {
let value = Number.parseFloat(preferences[key].size)
value = Math.min(value + add, payload.max)

preferences[key].size = value
}
})
/**
*
*/
onQueue() {
this.cli = {
lines: [],
error: '',
}

this.preferences.body = preferences
this.failed = false

this.status = 'pending'
}

/**
*
*/
onStart() {
this.status = 'running'
this.setupPreferences()

this.timer.start()

this.photo.events.emit('update')
this.status = 'running'
}

/**
*
*/
onFinish() {
this.status = 'finished'
this.timer.stop()

this._sendNotification()
this.photo.events.emit('update')
this.status = 'finished'

this.sendNotification()
achievements.probability()
}

/**
*
*/
onFail() {
this.status = 'finished'
this.failed = true

this.timer.stop()

this.photo.events.emit('update')
this.status = 'finished'
}

/**
* @return {Error}
*/
getPowerError() {
const errorMessage = this.cli.error

@@ -312,7 +343,10 @@ export class PhotoRun {
return new Exception(title, 'The algorithm has been interrupted by an unknown problem.', new Error(errorMessage), extra)
}

_sendNotification() {
/**
*
*/
sendNotification() {
if (!settings.notifications.run) {
return
}

+ 172
- 78
src/modules/nudify/photo.js View File

@@ -15,10 +15,12 @@ import MemoryStore from 'better-queue-memory'
import EventBus from 'js-event-bus'
import { settings } from '../system'
import { Consola, handleError } from '../consola'
import { NudifyQueue } from './queue'
import { Nudify } from './nudify'
import { PhotoRun } from './photo-run'
import { File } from '../file'
import { Timer } from '../timer'
import { events } from '../events'

const { getCurrentWindow } = require('electron').remote

@@ -66,7 +68,7 @@ export class Photo {

set status(value) {
this._status = value
Nudify.emitUpdate()
events.emit('nudify.update')
}

/**
@@ -89,6 +91,11 @@ export class Photo {
*/
timer = new Timer

/**
* @type {boolean}
*/
isMaskfin = false

/**
* @type {require('cropperjs').default}
*/
@@ -120,19 +127,19 @@ export class Photo {
}

get running() {
return this._status === 'running'
return this.status === 'running'
}

get finished() {
return this._status === 'finished'
return this.status === 'finished'
}

get pending() {
return this._status === 'pending'
return this.status === 'pending'
}

get waiting() {
return this._status === 'waiting'
return this.status === 'waiting'
}

get started() {
@@ -206,13 +213,14 @@ export class Photo {

this.consola = Consola.create(file.fullname)

this._setupPreferences(isMaskfin)
this.isMaskfin = isMaskfin

this._validate()

this._setupQueue()
this.setup()
}

/**
*
*/
async syncEditor() {
if (isNil(this.editor)) {
return
@@ -228,6 +236,9 @@ export class Photo {
this.consola.debug(`Saved editor changes.`)
}

/**
*
*/
async syncCrop() {
if (isNil(this.cropper)) {
return
@@ -251,15 +262,48 @@ export class Photo {
this.consola.debug(`Saved crop changes.`)
}

/**
*
* @param {...any} args
*/
getFolderPath(...args) {
return getModelsPath(this.folderName, ...args)
}

_setupPreferences(isMaskfin) {
/**
*
*/
setup() {
this.validate()

this.setupPreferences()

this.setupQueue()
}

/**
*
*/
validate() {
const { exists, mimetype, path } = this.file

if (!exists) {
throw new Warning('Upload failed.', `The file "${path}" does not exists.`)
}

if (mimetype !== 'image/jpeg' && mimetype !== 'image/png' && mimetype !== 'image/gif') {
throw new Warning('Upload failed.', `The file "${path}" is not a valid photo. Only jpeg, png or gif.`)
}
}

/**
*
*/
setupPreferences() {
this.preferences = cloneDeep(settings.payload.preferences)
let forcedPreferences = {}

if (isMaskfin) {
if (this.isMaskfin) {
forcedPreferences = {
body: {
executions: 1,
@@ -285,26 +329,17 @@ export class Photo {
this.preferences = merge(this.preferences, forcedPreferences)
}

_validate() {
const { exists, mimetype, path } = this.file

if (!exists) {
throw new Warning('Upload failed.', `The file "${path}" does not exists.`)
}

if (mimetype !== 'image/jpeg' && mimetype !== 'image/png' && mimetype !== 'image/gif') {
throw new Warning('Upload failed.', `The file "${path}" is not a valid photo. Only jpeg, png or gif.`)
}
}

_setupQueue() {
/**
*
*/
setupQueue() {
let maxTimeout = settings.processing.device === 'GPU' ? (3 * 60 * 1000) : (20 * 60 * 1000)

if (this.file.mimetype === 'image/gif') {
maxTimeout += (30 * 60 * 1000)
}

this.queue = new Queue(this._run, {
this.queue = new Queue(this.queueTicket, {
maxTimeout,
afterProcessDelay: 500,
batchSize: 1,
@@ -313,55 +348,82 @@ export class Photo {
})

this.queue.on('drain', () => {
this.consola.debug('All runs finished.')
this._onFinish()
this.onFinish()
this.consola.debug('Runs finished.')
})

this.queue.on('task_queued', (runId) => {
const run = this.getRunById(runId)
run.onQueue()

this.onQueueRun()

this.consola.debug(`Run added: #${runId}`)
})

this.queue.on('task_started', (runId, run) => {
this.consola.debug(`Run #${runId} started!`)
this.queue.on('task_started', (runId) => {
const run = this.getRunById(runId)
run.onStart()

this.consola.debug(`Run started: #${runId}`)
})

this.queue.on('task_finish', (runId) => {
const run = this.getRunById(runId)

this.consola.debug(`Run #${runId} finished!`)
run.onFinish()

this.consola.debug(`Run finished: #${runId}`)
})

this.queue.on('task_failed', (runId, error) => {
const run = this.getRunById(runId)

run.onFail()

this.consola.warn(`Run failed: #${runId} ${error}`)

if (isError(error)) {
handleError(error)
} else {
this.consola.warn(`Task failed with unknown error: ${error}`)
}
})
}

/**
*
* @param {*} id
*/
getRunById(id) {
return this.runs[id - 1]
}

addToQueue() {
Nudify.addToQueue(this)
}

removeFromQueue() {
Nudify.removeFromQueue(this)
/**
* Add this photo to the queue (time to nudify)
*/
add() {
NudifyQueue.add(this)
}

reset() {
this.status = 'pending'
/**
* Cancel the photo runs and remove it from the queue.
*/
cancel() {
NudifyQueue.cancel(this)

this.timer = new Timer
if (this.waiting) {
this.status = 'pending'
}
}

this.runs = []
/**
* Remove the photo from the application.
*/
forget() {
Nudify.forget(this)
}

/**
* Nudification start.
* This should only be called from the queue.
*/
async start() {
const { executions } = this.preferences.body

@@ -372,11 +434,7 @@ export class Photo {
await this.syncEditor()
await this.syncCrop()

this.reset()

this.consola.debug(`Starting ${executions} runs.`)

this._onStart()
// this.onStart()

for (let it = 1; it <= executions; it += 1) {
const run = new PhotoRun(it, this)
@@ -386,67 +444,103 @@ export class Photo {
}

await new Promise((resolve) => {
this.events.on('finish', () => {
this.queue.on('drain', () => {
resolve()
})
})
}

cancel(status = 'finished') {
/**
* Cancel the photo runs.
* This should only be called from the queue.
*/
stop() {
this.runs.forEach((run) => {
this.cancelRun(run)
})
}

this._onFinish(status)
/**
*
* @param {PhotoRun} run
*/
addRun(run) {
this.queue.push(run)
}

/**
*
* @param {PhotoRun} run
*/
cancelRun(run) {
this.queue.cancel(run.id)
}

rerun(run) {
run.reset()
this.queue.push(run)

this._onStart()
}

_run(run, cb) {
try {
run.start().then(() => {
cb()
return true
}).catch((error) => {
cb(error)
})
} catch (error) {
cb(error)
}
/**
*
* @param {PhotoRun} run
* @param {Function} done
*/
queueTicket(run, done) {
run.start().then(() => {
done()
return true
}).catch((error) => {
done(error)
})

return {
cancel() {
run.cancel()
run.stop()
},
}
}

_onStart() {
this.status = 'running'
/**
*
*/
onQueue() {
this.runs = []

this.status = 'waiting'
}

/**
*
*/
onStart() {
this.timer.start()

this.status = 'running'

this.events.emit('start')
}

_onFinish(status = 'finished') {
this.status = status
/**
*
*/
onQueueRun() {
this.timer.start()

this.status = 'running'
}

/**
*
*/
onFinish() {
this.timer.stop()
this.status = 'finished'

this.events.emit('finish')
this.sendNotification()

this._sendNotification()
this.events.emit('finish')
}

_sendNotification() {
/**
*
*/
sendNotification() {
if (!settings.notifications.allRuns) {
return
}

+ 109
- 0
src/modules/nudify/queue.js View File

@@ -0,0 +1,109 @@
// DreamTime.
// Copyright (C) DreamNet. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License 3.0 as published by
// the Free Software Foundation. See <https://www.gnu.org/licenses/gpl-3.0.html>
//
// Written by Ivan Bravo Bravo <ivan@dreamnet.tech>, 2020.

import { isError } from 'lodash'
import Queue from 'better-queue'
import MemoryStore from 'better-queue-memory'
import { Consola, handleError } from '../consola'

const consola = Consola.create('queue')

/**
*
*/
export const NudifyQueue = {
/**
* Queue. Photos that are waiting transformation.
* @type {Queue}
*/
queue: null,

/**
*
*/
setup() {
this.queue = new Queue(this.queueTicket, {
maxTimeout: (3 * 60 * 60 * 1000), // 3 hours.
afterProcessDelay: 1000,
batchSize: 1,
concurrent: 1,
store: new MemoryStore,
})

const { Nudify } = require('./nudify')

this.queue.on('task_queued', (photoId) => {
const photo = Nudify.getPhotoById(photoId)
photo.onQueue()

consola.debug(`Photo added: ${photoId}`)
})

this.queue.on('task_started', (photoId) => {
const photo = Nudify.getPhotoById(photoId)
photo.onStart()

consola.debug(`Photo started: #${photoId}`)
})

this.queue.on('task_finish', (photoId) => {
// const photo = Nudify.getPhotoById(photoId)
// photo.onFinish()

consola.debug(`Photo transformed: ${photoId}`)
})

this.queue.on('task_failed', (photoId, error) => {
const photo = Nudify.getPhotoById(photoId)
photo.onFinish()

consola.debug(`Photo failed: ${photoId} ${error}`)

if (isError(error)) {
handleError(error)
}
})
},

/**
*
* @param {Photo} photo
* @param {Function} done
*/
queueTicket(photo, done) {
photo.start().then(() => {
done()
return true
}).catch((error) => {
done(error)
})

return {
cancel() {
photo.stop()
},
}
},

/**
*
* @param {Photo} photo
*/
add(photo) {
this.queue.push(photo)
},

/**
*
* @param {Photo} photo
*/
cancel(photo) {
this.queue.cancel(photo.id)
},
}

+ 42
- 9
src/modules/timer.js View File

@@ -1,32 +1,65 @@
// DreamTime.
// Copyright (C) DreamNet. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License 3.0 as published by
// the Free Software Foundation. See <https://www.gnu.org/licenses/gpl-3.0.html>
//
// Written by Ivan Bravo Bravo <ivan@dreamnet.tech>, 2020.

import { isNil } from 'lodash'
import dayjs from 'dayjs'
import { Consola } from './consola'

const consola = Consola.create('timer')

export class Timer {
constructor() {
this.startTime = undefined
startTime

endTime

duration = 0

interval

free() {
clearInterval(this.interval)
this.interval = null
}

reset() {
this.startTime = null

this.endTime = null

this.duration = 0
this.interval = undefined

this.free()
}

start() {
this.reset()

this.startTime = dayjs()
this.duration = 0
this.interval = setInterval(this.handle.bind(this), 1000)
this.interval = setInterval(this.update.bind(this), 500)
}

handle() {
update() {
this.duration = dayjs().diff(this.startTime, 'second')

if (this.duration > 3600) {
// this does not seem normal.
if (this.duration > 10800) {
// This does not seem normal.
consola.warn('Timer out of control.').report()
this.stop()
}
}

stop() {
clearInterval(this.interval)
this.free()

this.endTime = dayjs()

this.duration = this.endTime.diff(this.startTime, 'second')
}
}

+ 0
- 1
src/nuxt.config.js View File

@@ -1,5 +1,4 @@
const dev = process.env.NODE_ENV === 'development'

const analyze = false
const uglify = !dev
const cache = !uglify && dev

+ 3
- 1
src/package.json View File

@@ -4,7 +4,7 @@
"description": "An open-source and super-improved version of DeepNude.",
"author": "DreamNet",
"homepage": "https://time.dreamnet.tech",
"version": "1.3.2",
"version": "1.3.3",
"main": "electron/dist/index.js",
"license": "GPL-3.0-only",
"private": true,
@@ -85,6 +85,7 @@
"lodash": "^4.17.15",
"logrocket": "^1.0.5",
"md5-file": "^4.0.0",
"melanke-watchjs": "^1.5.0",
"mime-types": "^2.1.25",
"node-7z": "^2.0.3",
"nucleus-nodejs": "^3.0.1",
@@ -141,6 +142,7 @@
"eslint-plugin-standard": ">=4.0.1",
"eslint-plugin-vue": "^6.0.1",
"fibers": "^4.0.2",
"flow-bin": "^0.115.0",
"husky": "^3.0.9",
"lint-staged": "^9.4.3",
"mocha": "^6.2.2",

+ 1
- 1
src/package.min.json View File

@@ -4,7 +4,7 @@
"description": "An open-source and super-improved version of DeepNude.",
"author": "DreamNet",
"homepage": "https://time.dreamnet.tech",
"version": "1.3.2",
"version": "1.3.3",
"main": "electron/dist/index.js",
"license": "GPL-3.0-only",
"private": true

+ 12
- 27
src/pages/nudify/_id.vue View File

@@ -40,12 +40,12 @@
</div>

<!-- Buttons -->
<button v-show="!photo.running && !photo.waiting" class="button button--success" @click.prevent="start">
<button v-show="!photo.running && !photo.waiting" class="button button--success" @click.prevent="add">
<span class="icon"><font-awesome-icon icon="play" /></span>
<span>Add to queue</span>
</button>

<button v-show="photo.waiting" class="button button--danger" @click.prevent="removeFromQueue">
<button v-show="photo.waiting" class="button button--danger" @click.prevent="cancel">
<span>Remove from queue</span>
</button>

@@ -74,7 +74,6 @@

<script>
import { isNil } from 'lodash'
import Swal from 'sweetalert2/dist/sweetalert2.js'
import { Nudify } from '~/modules/nudify'

const { shell } = $provider.api
@@ -88,7 +87,7 @@ export default {
return
}

const photo = Nudify.getPhoto(params.id)
const photo = Nudify.getPhotoById(params.id)

if (isNil(photo)) {
redirect('/')
@@ -110,15 +109,19 @@ export default {

created() {
const { params } = this.$route
this.photo = Nudify.getPhoto(params.id)
this.photo = Nudify.getPhotoById(params.id)
},

methods: {
start() {
this.photo.addToQueue()
add() {
this.photo.add()
this.$router.push(`/nudify/${this.photo.id}/results`)
},

cancel() {
this.photo.cancel()
},

stop() {
this.photo.cancel()
},
@@ -127,27 +130,9 @@ export default {
shell.openItem(this.photo.getFolderPath())
},

removeFromQueue() {
this.photo.removeFromQueue()
},

async forget() {
const response = await Swal.fire({
title: 'Are you sure?',
text: 'Forgetting the photo will remove it from the queue (but not the files) and free up memory.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#F44336',
confirmButtonText: 'Yes, forget it',
})

if (!response.value) {
return
}

Nudify.forget(this.photo)

forget() {
this.$router.push(`/`)
this.photo.forget()
},
},
}

+ 1
- 1
src/pages/nudify/_id/results.vue View File

@@ -59,7 +59,7 @@ export default {
.runs {
@apply flex flex-wrap justify-between;

.c-photo-run {
.photo-run {
@apply mb-2;
width: calc(1/2*100% - (1 - 1/2)*1rem);


Loading…
Cancel
Save