/** * 转换器工具类 */ export const Converter = { /** * 添加转换按钮 * @param type - 组件类型 */ addPromptButton(type: string): void { console.debug('🤯 [formatPrompt] inject', type); const actionsColumn: HTMLElement | null = gradioApp().querySelector( `#${type}_tools > div.form`, ); const formatBtn: HTMLElement | null = gradioApp().querySelector(`#${type}_formatconvert`); if (!actionsColumn || formatBtn) return; const convertButton: HTMLElement = Converter.createButton(`${type}_formatconvert`, '🪄', () => Converter.onClickConvert(type)); actionsColumn.append(convertButton); }, /** * 将输入的字符串转换成特定格式的字符串 * @param input 输入的字符串 * @returns 转换后的字符串 */ convert(input: string): string { const re_attention = /\{|\[|\}|\]|[^{}[\]]+/gmu; let text = Converter.convertStr(input); const textArray = Converter.convertStr2Array(text); text = Converter.convertArray2Str(textArray); let res: [string, number][] = []; const curly_bracket_multiplier = 1.05; const square_bracket_multiplier = 1 / 1.05; const brackets: Record = { '[': { multiplier: square_bracket_multiplier, stack: [] }, '{': { multiplier: curly_bracket_multiplier, stack: [] }, }; /** * 将指定范围内的数字乘以指定倍数 * @param start_position 起始位置 * @param multiplier 倍数 */ function multiply_range(start_position: number, multiplier: number) { for (let pos = start_position; pos < res.length; pos++) { res[pos][1] = Converter.round(res[pos][1] * multiplier); } } for (const match of text.matchAll(re_attention)) { let word = match[0]; if (word in brackets) { brackets[word].stack.push(res.length); } else if (word === '}' || word === ']') { const bracket = brackets[word === '}' ? '{' : '[']; if (bracket.stack.length > 0) { multiply_range(bracket.stack.pop()!, bracket.multiplier); } } else { res.push([word, 1]); } } for (const bracketType of Object.keys(brackets)) { for (const pos of brackets[bracketType].stack) { multiply_range(pos, brackets[bracketType].multiplier); } } if (res.length === 0) { res = [['', 1]]; } let index = 0; while (index + 1 < res.length) { if (res[index][1] === res[index + 1][1]) { res[index][0] += res[index + 1][0]; res.splice(index + 1, 1); } else { index += 1; } } let result = ''; for (const [word, value] of res) { result += value === 1 ? word : `(${word}:${value.toString()})`; } return result; }, /** * 将数组转换成字符串 * @param array 数组 * @returns 转换后的字符串 */ convertArray2Str(array: string[]): string { const newArray = array.map((item) => { if (item.includes('<')) return item; const newItem = item .replaceAll(/\s+/g, ' ') .replaceAll(/,|\.\|。/g, ',') .replaceAll(/“|‘|”|"|\/'/g, '') .replaceAll(', ', ',') .replaceAll(',,', ',') .replaceAll(',', ', '); return Converter.convertStr2Array(newItem).join(', '); }); return newArray.join(', '); }, /** * 将字符串中的中文冒号和括号转换成英文冒号和括号 * @param srt 字符串 * @returns 转换后的字符串 */ convertStr(srt: string): string { return srt.replaceAll(':', ':').replaceAll('(', '(').replaceAll(')', ')'); }, /** * 将字符串按照括号分割成数组 * @param str 字符串 * @returns 分割后的数组 */ convertStr2Array(string_: string): string[] { // 匹配各种括号中的内容,包括括号本身 const bracketRegex = /([()<>[\]])/g; /** * 将字符串按照各种括号分割成数组 * @param str 字符串 * @returns 分割后的数组 */ const splitByBracket = (string__: string): string[] => { const array: string[] = []; let start = 0; let depth = 0; let match; while ((match = bracketRegex.exec(string__)) !== null) { if (depth === 0 && match.index > start) { array.push(string__.slice(start, match.index)); start = match.index; } if (match[0] === '(' || match[0] === '<' || match[0] === '[') { depth++; } else if (match[0] === ')' || match[0] === '>' || match[0] === ']') { depth--; } if (depth === 0) { array.push(string__.slice(start, match.index + 1)); start = match.index + 1; } } if (start < string__.length) { array.push(string__.slice(Math.max(0, start))); } return array; }; /** * 将字符串按照逗号和各种括号分割成数组 * @param str 字符串 * @returns 分割后的数组 */ const splitByComma = (string__: string): string[] => { const array: string[] = []; let start = 0; let inBracket = false; for (let index = 0; index < string__.length; index++) { if (string__[index] === ',' && !inBracket) { array.push(string__.slice(start, index).trim()); start = index + 1; } else if (bracketRegex.test(string__[index])) { inBracket = !inBracket; } } array.push(string__.slice(Math.max(0, start)).trim()); return array; }; /** * 清洗字符串并输出数组 * @param str 字符串 * @returns 清洗后的数组 */ const cleanString = (string__: string): string[] => { let array = splitByBracket(string__); array = array.flatMap((s) => splitByComma(s)); return array.filter((s) => s !== ''); }; return cleanString(string_) .filter((item) => { const pattern = /^[\s,,]+$/; return !pattern.test(item); }) .filter(Boolean) .sort((a, b) => { return a.includes('<') && !b.includes('<') ? 1 : b.includes('<') && !a.includes('<') ? -1 : 0; }); }, /** * 创建转换按钮 * @param id 按钮 id * @param innerHTML 按钮文本 * @param onClick 点击事件处理函数 * @returns 新建的按钮元素 */ createButton(id: string, innerHTML: string, onClick: () => void): HTMLButtonElement { const button = document.createElement('button'); button.id = id; button.type = 'button'; button.innerHTML = innerHTML; button.title = 'Format prompt~🪄'; button.className = 'lg secondary gradio-button tool svelte-cmf5ev'; button.addEventListener('click', onClick); return button; }, /** * 触发 input 事件 * @param target 目标元素 */ dispatchInputEvent(target: EventTarget) { let inputEvent = new Event('input'); Object.defineProperty(inputEvent, 'target', { value: target }); target.dispatchEvent(inputEvent); }, /** * 点击转换按钮的事件处理函数 * @param type 类型 */ onClickConvert(type: string) { const default_prompt = ''; const default_negative = ''; const prompt = gradioApp().querySelector( `#${type}_prompt > label > textarea`, ) as HTMLTextAreaElement; const result = Converter.convert(prompt.value); prompt.value = result.match(/^masterpiece, best quality,/) === null ? default_prompt + result : result; Converter.dispatchInputEvent(prompt); const negprompt = gradioApp().querySelector( `#${type}_neg_prompt > label > textarea`, ) as HTMLTextAreaElement; const negResult = Converter.convert(negprompt.value); negprompt.value = negResult.match(/^lowres,/) === null ? negResult.length === 0 ? default_negative : default_negative + negResult : negResult; Converter.dispatchInputEvent(negprompt); }, /** * 将数字四舍五入到小数点后四位 * @param value 数字 * @returns 四舍五入后的数字 */ round(value: number): number { return Math.round(value * 10_000) / 10_000; }, }; export default () => { console.time('🤯 [formatPrompt] inject'); Converter.addPromptButton('txt2img'); Converter.addPromptButton('img2img'); console.timeEnd('🤯 [formatPrompt] inject'); };