mirror of
https://github.com/koishijs/novelai-bot
synced 2025-01-09 04:37:54 +08:00
feat: support computed config
This commit is contained in:
parent
42f96a459a
commit
044b4f12ce
12
package.json
12
package.json
@ -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",
|
||||
|
@ -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(', ')]
|
||||
|
25
src/index.ts
25
src/index.ts
@ -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())
|
||||
|
Loading…
Reference in New Issue
Block a user