feat: support computed config

This commit is contained in:
Shigma 2023-02-16 22:53:25 +08:00
parent 42f96a459a
commit 044b4f12ce
No known key found for this signature in database
GPG Key ID: 21C89B0B92907E14
3 changed files with 53 additions and 62 deletions

View File

@ -1,7 +1,7 @@
{
"name": "koishi-plugin-novelai",
"description": "Generate images by diffusion models",
"version": "1.16.0",
"version": "1.16.4",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"files": [
@ -63,19 +63,19 @@
"generate"
],
"peerDependencies": {
"koishi": "^4.11.0"
"koishi": "^4.11.6"
},
"devDependencies": {
"@koishijs/plugin-help": "^2.0.1",
"@koishijs/plugin-help": "^2.0.3",
"@koishijs/translator": "^1.0.0",
"@koishijs/vitepress": "^1.5.3",
"@koishijs/vitepress": "^1.6.4",
"@types/libsodium-wrappers": "^0.7.10",
"@types/node": "^17.0.45",
"atsc": "^1.2.2",
"koishi": "^4.11.0",
"koishi": "^4.11.6",
"sass": "^1.57.1",
"typescript": "^4.9.4",
"vitepress": "1.0.0-alpha.26"
"vitepress": "1.0.0-alpha.34"
},
"dependencies": {
"image-size": "^1.0.2",

View File

@ -1,4 +1,4 @@
import { Dict, Schema, Time } from 'koishi'
import { Computed, Dict, Schema, Session, Time } from 'koishi'
import { Size } from './utils'
export const modelMap = {
@ -129,40 +129,40 @@ export interface Options {
}
export interface PromptConfig {
basePrompt?: string
negativePrompt?: string
forbidden?: string
placement?: 'before' | 'after'
latinOnly?: boolean
basePrompt?: Computed<string>
negativePrompt?: Computed<string>
forbidden?: Computed<string>
placement?: Computed<'before' | 'after'>
latinOnly?: Computed<boolean>
translator?: boolean
maxWords?: number
maxWords?: Computed<number>
}
export const PromptConfig: Schema<PromptConfig> = Schema.object({
basePrompt: Schema.string().role('textarea').description('默认附加的标签。').default('masterpiece, best quality'),
negativePrompt: Schema.string().role('textarea').description('默认附加的反向标签。').default(ucPreset),
forbidden: Schema.string().role('textarea').description('违禁词列表。请求中的违禁词将会被自动删除。').default(''),
placement: Schema.union([
basePrompt: Schema.computed(Schema.string().role('textarea')).description('默认附加的标签。').default('masterpiece, best quality'),
negativePrompt: Schema.computed(Schema.string().role('textarea')).description('默认附加的反向标签。').default(ucPreset),
forbidden: Schema.computed(Schema.string().role('textarea')).description('违禁词列表。请求中的违禁词将会被自动删除。').default(''),
placement: Schema.computed(Schema.union([
Schema.const('before' as const).description('置于最前'),
Schema.const('after' as const).description('置于最后'),
]).description('默认附加标签的位置。').default('after'),
])).description('默认附加标签的位置。').default('after'),
translator: Schema.boolean().description('是否启用自动翻译。').default(true),
latinOnly: Schema.boolean().description('是否只接受英文输入。').default(false),
maxWords: Schema.natural().description('允许的最大单词数量。').default(0),
latinOnly: Schema.computed(Schema.boolean()).description('是否只接受英文输入。').default(false),
maxWords: Schema.computed(Schema.natural()).description('允许的最大单词数量。').default(0),
}).description('输入设置')
interface ParamConfig {
model?: Model
upscaler?: string
resolution?: Orient | Size
maxResolution?: number
sampler?: string
scale?: number
textSteps?: number
imageSteps?: number
maxSteps?: number
upscaler?: string
restoreFaces?: boolean
hiresFix?: boolean
scale?: Computed<number>
textSteps?: Computed<number>
imageSteps?: Computed<number>
maxSteps?: Computed<number>
resolution?: Computed<Orient | Size>
maxResolution?: Computed<number>
}
export interface Config extends PromptConfig, ParamConfig {
@ -192,7 +192,7 @@ export const Config = Schema.intersect([
Schema.const('naifu' as const).description('naifu'),
Schema.const('sd-webui' as const).description('sd-webui'),
Schema.const('stable-horde' as const).description('Stable Horde'),
] as const).description('登录方式'),
] as const).description('登录方式'),
}).description('登录设置'),
Schema.union([
@ -210,7 +210,7 @@ export const Config = Schema.intersect([
]),
Schema.object({
endpoint: Schema.string().description('API 服务器地址。').default('https://api.novelai.net'),
headers: Schema.dict(String).description('要附加的额外请求头。').default({
headers: Schema.dict(String).role('table').description('要附加的额外请求头。').default({
'referer': 'https://novelai.net/',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36',
}),
@ -220,12 +220,12 @@ export const Config = Schema.intersect([
type: Schema.const('naifu'),
token: Schema.string().description('授权令牌。').role('secret'),
endpoint: Schema.string().description('API 服务器地址。').required(),
headers: Schema.dict(String).description('要附加的额外请求头。'),
headers: Schema.dict(String).role('table').description('要附加的额外请求头。'),
}),
Schema.object({
type: Schema.const('sd-webui'),
endpoint: Schema.string().description('API 服务器地址。').required(),
headers: Schema.dict(String).description('要附加的额外请求头。'),
headers: Schema.dict(String).role('table').description('要附加的额外请求头。'),
}),
Schema.object({
type: Schema.const('stable-horde'),
@ -253,7 +253,7 @@ export const Config = Schema.intersect([
type: Schema.const('stable-horde'),
sampler: sampler.createSchema(sampler.horde),
model: Schema.union(hordeModels),
}),
}).description('参数设置'),
Schema.object({
type: Schema.const('naifu'),
sampler: sampler.createSchema(sampler.nai),
@ -265,10 +265,10 @@ export const Config = Schema.intersect([
] as const),
Schema.object({
scale: Schema.number().description('默认对输入的服从度。').default(11),
textSteps: Schema.natural().description('文本生图时默认的迭代步数。').default(28),
imageSteps: Schema.natural().description('以图生图时默认的迭代步数。').default(50),
maxSteps: Schema.natural().description('允许的最大迭代步数。').default(64),
scale: Schema.computed(Schema.number()).description('默认对输入的服从度。').default(11),
textSteps: Schema.computed(Schema.natural()).description('文本生图时默认的迭代步数。').default(28),
imageSteps: Schema.computed(Schema.natural()).description('以图生图时默认的迭代步数。').default(50),
maxSteps: Schema.computed(Schema.natural()).description('允许的最大迭代步数。').default(64),
resolution: Schema.union([
Schema.const('portrait' as const).description('肖像 (768x512)'),
Schema.const('landscape' as const).description('风景 (512x768)'),
@ -278,7 +278,7 @@ export const Config = Schema.intersect([
height: Schema.natural().description('图片高度。').default(640),
}).description('自定义'),
] as const).description('默认生成的图片尺寸。').default('portrait'),
maxResolution: Schema.natural().description('允许生成的宽高最大值。').default(1024),
maxResolution: Schema.computed(Schema.natural()).description('允许生成的宽高最大值。').default(1024),
}),
PromptConfig,
@ -319,7 +319,7 @@ export function parseForbidden(input: string) {
const backslash = /@@__BACKSLASH__@@/g
export function parseInput(input: string, config: Config, forbidden: Forbidden[], override: boolean): string[] {
export function parseInput(session: Session, input: string, config: Config, override: boolean): string[] {
input = input.toLowerCase()
.replace(/\\\\/g, backslash.source)
.replace(//g, ',')
@ -340,18 +340,19 @@ export function parseInput(input: string, config: Config, forbidden: Forbidden[]
.replace(backslash, '\\')
.replace(/_/g, ' ')
if (config.latinOnly && /[^\s\w"'“”‘’.,:|\\()\[\]{}-]/.test(input)) {
if (session.resolve(config.latinOnly) && /[^\s\w"'“”‘’.,:|\\()\[\]{}-]/.test(input)) {
return ['.latin-only']
}
const negative = []
const placement = session.resolve(config.placement)
const appendToList = (words: string[], input: string) => {
const tags = input.split(/,\s*/g)
if (config.placement === 'before') tags.reverse()
if (placement === 'before') tags.reverse()
for (let tag of tags) {
tag = tag.trim().toLowerCase()
if (!tag || words.includes(tag)) continue
if (config.placement === 'before') {
if (placement === 'before') {
words.unshift(tag)
} else {
words.push(tag)
@ -367,6 +368,7 @@ export function parseInput(input: string, config: Config, forbidden: Forbidden[]
}
// remove forbidden words
const forbidden = parseForbidden(session.resolve(config.forbidden))
const positive = input.split(/,\s*/g).filter((word) => {
// eslint-disable-next-line no-control-regex
word = word.replace(/[\x00-\x7f]/g, s => s.replace(/[^0-9a-zA-Z]/, ' ')).replace(/\s+/, ' ').trim()
@ -381,13 +383,13 @@ export function parseInput(input: string, config: Config, forbidden: Forbidden[]
return true
})
if (Math.max(getWordCount(positive), getWordCount(negative)) > (config.maxWords || Infinity)) {
if (Math.max(getWordCount(positive), getWordCount(negative)) > (session.resolve(config.maxWords) || Infinity)) {
return ['.too-many-words']
}
if (!override) {
appendToList(positive, config.basePrompt)
appendToList(negative, config.negativePrompt)
appendToList(positive, session.resolve(config.basePrompt))
appendToList(negative, session.resolve(config.negativePrompt))
}
return [null, positive.join(', '), negative.join(', ')]

View File

@ -1,5 +1,5 @@
import { Context, Dict, Logger, omit, Quester, segment, Session, SessionError, trimSlash } from 'koishi'
import { Config, modelMap, models, orientMap, parseForbidden, parseInput, sampler, upscalers } from './config'
import { Config, modelMap, models, orientMap, parseInput, sampler, upscalers } from './config'
import { ImageData, StableDiffusionWebUI } from './types'
import { closestMultiple, download, forceDataPrefix, getImageSize, login, NetworkError, project, resizeInput, Size } from './utils'
import {} from '@koishijs/translator'
@ -28,11 +28,6 @@ function handleError(session: Session, err: Error) {
return session.text('.unknown-error')
}
interface Forbidden {
pattern: string
strict: boolean
}
export function apply(ctx: Context, config: Config) {
ctx.i18n.define('zh', require('./locales/zh-CN'))
ctx.i18n.define('zh-TW', require('./locales/zh-TW'))
@ -40,14 +35,9 @@ export function apply(ctx: Context, config: Config) {
ctx.i18n.define('fr', require('./locales/fr-FR'))
ctx.i18n.define('ja', require('./locales/ja-JP'))
let forbidden: Forbidden[]
const tasks: Dict<Set<string>> = Object.create(null)
const globalTasks = new Set<string>()
ctx.accept(['forbidden'], (config) => {
forbidden = parseForbidden(config.forbidden)
}, { immediate: true })
let tokenTask: Promise<string> = null
const getToken = () => tokenTask ||= login(ctx)
ctx.accept(['token', 'type', 'email', 'password'], () => tokenTask = null)
@ -68,9 +58,9 @@ export function apply(ctx: Context, config: Config) {
return args.some(callback => callback(session))
}
const step = (source: string) => {
const step = (source: string, session: Session) => {
const value = +source
if (value * 0 === 0 && Math.floor(value) === value && value > 0 && value <= (config.maxSteps || Infinity)) return value
if (value * 0 === 0 && Math.floor(value) === value && value > 0 && value <= session.resolve(config.maxSteps || Infinity)) return value
throw new Error()
}
@ -80,7 +70,7 @@ export function apply(ctx: Context, config: Config) {
if (!cap) throw new Error()
const width = closestMultiple(+cap[1])
const height = closestMultiple(+cap[2])
if (Math.max(width, height) > (config.maxResolution || Infinity)) {
if (Math.max(width, height) > session.resolve(config.maxResolution || Infinity)) {
throw new SessionError('commands.novelai.messages.invalid-resolution')
}
return { width, height, custom: true }
@ -157,7 +147,7 @@ export function apply(ctx: Context, config: Config) {
}
}
const [errPath, prompt, uc] = parseInput(input, config, forbidden, options.override)
const [errPath, prompt, uc] = parseInput(session, input, config, options.override)
if (errPath) return session.text(errPath)
let token: string
@ -184,8 +174,8 @@ export function apply(ctx: Context, config: Config) {
// 2: none
ucPreset: 2,
qualityToggle: false,
scale: options.scale ?? 11,
steps: options.steps ?? (imgUrl ? config.imageSteps : config.textSteps),
scale: options.scale ?? session.resolve(config.scale),
steps: options.steps ?? session.resolve(imgUrl ? config.imageSteps : config.textSteps),
}
if (imgUrl) {
@ -464,7 +454,6 @@ export function apply(ctx: Context, config: Config) {
}
cmd._options.output.fallback = config.output
cmd._options.scale.fallback = config.scale
cmd._options.model.fallback = config.model
cmd._options.sampler.fallback = config.sampler
cmd._options.sampler.type = Object.keys(getSamplers())