| 1 | const DRAFT_PROMPT_LIMIT_PREFIX = '重要:' |
| 2 | const DRAFT_PROMPT_LIMIT_SEPARATOR = ',' |
| 3 | |
| 4 | export const DRAFT_TITLE_NO_EMOJI_LIMIT = '标题一定不要加表情。' |
| 5 | |
| 6 | interface DraftPromptLimitTextOptions { |
| 7 | prefix?: string |
| 8 | separator?: string |
| 9 | } |
| 10 | |
| 11 | interface CaptionPromptPriorityOptions { |
| 12 | userRequirementTitle: string |
| 13 | systemRequirementTitle: string |
| 14 | } |
| 15 | |
| 16 | interface StripCaptionPromptSystemRequirementOptions { |
| 17 | userRequirementTitle?: string |
| 18 | systemRequirementTitle?: string |
| 19 | systemRequirementPrefix?: string |
| 20 | } |
| 21 | |
| 22 | const DRAFT_PROMPT_LIMIT_PATTERNS = [ |
| 23 | /^标题字数限制:.+$/, |
| 24 | /^描述字数限制:.+$/, |
| 25 | /^话题数量限制:.+$/, |
| 26 | /^标题字数不超过.+$/, |
| 27 | /^描述字数不超过.+$/, |
| 28 | /^话题数量不超过.+$/, |
| 29 | /^标题一定不要加表情。?$/, |
| 30 | ] |
| 31 | |
| 32 | const CAPTION_PROMPT_SYSTEM_REQUIREMENT_PREFIXES = ['重要:', 'Important:', 'Important :', 'Wichtig:', '중요:'] |
| 33 | const CAPTION_PROMPT_USER_SECTION_TITLE_PATTERNS = [ |
| 34 | /用户文案要求/, |
| 35 | /User Caption Requirements/i, |
| 36 | /Benutzeranforderungen/i, |
| 37 | /Exigences utilisateur/i, |
| 38 | /ユーザー文案要件/, |
| 39 | /사용자 문안 요구사항/, |
| 40 | ] |
| 41 | const CAPTION_PROMPT_SYSTEM_SECTION_TITLE_PATTERNS = [ |
| 42 | /默认提示词/, |
| 43 | /Default Prompt/i, |
| 44 | /Standard-Prompt/i, |
| 45 | /Prompt par défaut/i, |
| 46 | /デフォルトプロンプト/, |
| 47 | /기본 프롬프트/, |
| 48 | ] |
| 49 | const CAPTION_PROMPT_SECTION_TITLE_WRAPPER_PATTERN = /^(?:【.+】|\[.+\])$/ |
| 50 | const CAPTION_PROMPT_SYSTEM_LIMIT_PATTERNS = [ |
| 51 | /标题字数/, |
| 52 | /描述字数/, |
| 53 | /话题数量/, |
| 54 | /标题一定不要加表情/, |
| 55 | /Title must be no more/i, |
| 56 | /Description must be no more/i, |
| 57 | /Use no more than .* topics/i, |
| 58 | /Do not use emoji in the title/i, |
| 59 | /Der Titel darf höchstens/i, |
| 60 | /Die Beschreibung darf höchstens/i, |
| 61 | /höchstens .* Themen/i, |
| 62 | /Keine Emojis im Titel/i, |
| 63 | /Le titre ne doit pas dépasser/i, |
| 64 | /La description ne doit pas dépasser/i, |
| 65 | /au maximum .* sujets/i, |
| 66 | /emoji dans le titre/i, |
| 67 | /タイトルは.*文字以内/, |
| 68 | /説明は.*文字以内/, |
| 69 | /トピックは.*個以内/, |
| 70 | /タイトルに絵文字/, |
| 71 | /제목은 .*자 이내/, |
| 72 | /설명은 .*자 이내/, |
| 73 | /주제는 최대 .*개/, |
| 74 | /제목에는 이모지/, |
| 75 | ] |
| 76 | |
| 77 | function getLastDraftPromptLimitMarkerIndex(prompt: string) { |
| 78 | return Math.max( |
| 79 | prompt.lastIndexOf(`\n\n${DRAFT_PROMPT_LIMIT_PREFIX}`), |
| 80 | prompt.lastIndexOf(`\r\n\r\n${DRAFT_PROMPT_LIMIT_PREFIX}`), |
| 81 | ) |
| 82 | } |
| 83 | |
| 84 | function getDraftPromptLimitMarkerLength(prompt: string, markerIndex: number) { |
| 85 | return prompt.startsWith(`\r\n\r\n${DRAFT_PROMPT_LIMIT_PREFIX}`, markerIndex) |
| 86 | ? `\r\n\r\n${DRAFT_PROMPT_LIMIT_PREFIX}`.length |
| 87 | : `\n\n${DRAFT_PROMPT_LIMIT_PREFIX}`.length |
| 88 | } |
| 89 | |
| 90 | function isDraftPromptLimit(limit: string) { |
| 91 | return DRAFT_PROMPT_LIMIT_PATTERNS.some(pattern => pattern.test(limit)) |
| 92 | } |
| 93 | |
| 94 | function normalizePromptLineBreaks(prompt: string) { |
| 95 | return prompt.replace(/\r\n/g, '\n').replace(/\r/g, '\n') |
| 96 | } |
| 97 | |
| 98 | function isCaptionPromptSectionTitle(line: string, patterns: RegExp[]) { |
| 99 | const title = line.trim() |
| 100 | return CAPTION_PROMPT_SECTION_TITLE_WRAPPER_PATTERN.test(title) |
| 101 | && patterns.some(pattern => pattern.test(title)) |
| 102 | } |
| 103 | |
| 104 | function getFirstBlankLineMatch(prompt: string) { |
| 105 | return /\n\s*\n/.exec(prompt) |
| 106 | } |
| 107 | |
| 108 | function getCaptionPromptSectionText(lines: string[], startIndex: number, endIndex: number) { |
| 109 | return lines.slice(startIndex + 1, endIndex).join('\n').trim() |
| 110 | } |
| 111 | |
| 112 | function isSystemRequirementBlock(prompt: string, prefixes: string[]) { |
| 113 | if (!prefixes.some(prefix => prompt.startsWith(prefix))) |
| 114 | return false |
| 115 | |
| 116 | return CAPTION_PROMPT_SYSTEM_LIMIT_PATTERNS.reduce((count, pattern) => ( |
| 117 | pattern.test(prompt) ? count + 1 : count |
| 118 | ), 0) >= 2 |
| 119 | } |
| 120 | |
| 121 | function stripLeadingSystemRequirementBlock(prompt: string, prefixes: string[]) { |
| 122 | const blankLineMatch = getFirstBlankLineMatch(prompt) |
| 123 | if (!blankLineMatch) |
| 124 | return isSystemRequirementBlock(prompt, prefixes) ? '' : null |
| 125 | |
| 126 | const firstBlock = prompt.slice(0, blankLineMatch.index).trim() |
| 127 | if (!isSystemRequirementBlock(firstBlock, prefixes)) |
| 128 | return null |
| 129 | |
| 130 | return prompt.slice(blankLineMatch.index + blankLineMatch[0].length).trim() |
| 131 | } |
| 132 | |
| 133 | export function buildDraftPromptLimitText(limits: string[], options: DraftPromptLimitTextOptions = {}) { |
| 134 | const normalizedLimits = limits.map(limit => limit.trim()).filter(Boolean) |
| 135 | if (normalizedLimits.length === 0) |
| 136 | return '' |
| 137 | |
| 138 | const prefix = options.prefix ?? DRAFT_PROMPT_LIMIT_PREFIX |
| 139 | const separator = options.separator ?? DRAFT_PROMPT_LIMIT_SEPARATOR |
| 140 | return `${prefix}${normalizedLimits.join(separator)}` |
| 141 | } |
| 142 | |
| 143 | export function buildPromptWithDraftLimits( |
| 144 | basePrompt: string, |
| 145 | limits: string[], |
| 146 | options: DraftPromptLimitTextOptions = {}, |
| 147 | ) { |
| 148 | const limitText = buildDraftPromptLimitText(limits, options) |
| 149 | if (!limitText) |
| 150 | return basePrompt |
| 151 | |
| 152 | return `${basePrompt}\n\n${limitText}` |
| 153 | } |
| 154 | |
| 155 | export function mergeCaptionPromptWithSystemRequirement( |
| 156 | captionPrompt: string, |
| 157 | systemRequirement: string, |
| 158 | options?: CaptionPromptPriorityOptions, |
| 159 | ) { |
| 160 | const normalizedSystemRequirement = systemRequirement.trim() |
| 161 | const normalizedCaptionPrompt = captionPrompt.trim() |
| 162 | |
| 163 | if (!normalizedSystemRequirement) |
| 164 | return normalizedCaptionPrompt |
| 165 | |
| 166 | if (!normalizedCaptionPrompt) |
| 167 | return normalizedSystemRequirement |
| 168 | |
| 169 | if (!options) |
| 170 | return `${normalizedSystemRequirement}\n\n${normalizedCaptionPrompt}` |
| 171 | |
| 172 | return [ |
| 173 | `${options.userRequirementTitle.trim()}\n${normalizedCaptionPrompt}`, |
| 174 | `${options.systemRequirementTitle.trim()}\n${normalizedSystemRequirement}`, |
| 175 | ].filter(Boolean).join('\n\n') |
| 176 | } |
| 177 | |
| 178 | export function stripCaptionPromptSystemRequirement( |
| 179 | captionPrompt: string, |
| 180 | options: StripCaptionPromptSystemRequirementOptions = {}, |
| 181 | ) { |
| 182 | const normalizedCaptionPrompt = normalizePromptLineBreaks(captionPrompt).trim() |
| 183 | if (!normalizedCaptionPrompt) |
| 184 | return '' |
| 185 | |
| 186 | const lines = normalizedCaptionPrompt.split('\n') |
| 187 | const userRequirementTitle = options.userRequirementTitle?.trim() |
| 188 | const systemRequirementTitle = options.systemRequirementTitle?.trim() |
| 189 | |
| 190 | if (userRequirementTitle && systemRequirementTitle) { |
| 191 | const userTitleIndex = lines.findIndex(line => line.trim() === userRequirementTitle) |
| 192 | const systemTitleIndex = lines.findIndex((line, index) => index > userTitleIndex && line.trim() === systemRequirementTitle) |
| 193 | if (userTitleIndex >= 0 && systemTitleIndex > userTitleIndex) |
| 194 | return getCaptionPromptSectionText(lines, userTitleIndex, systemTitleIndex) |
| 195 | |
| 196 | if (userTitleIndex >= 0) |
| 197 | return lines.slice(userTitleIndex + 1).join('\n').trim() |
| 198 | } |
| 199 | |
| 200 | const userTitleIndex = lines.findIndex(line => isCaptionPromptSectionTitle(line, CAPTION_PROMPT_USER_SECTION_TITLE_PATTERNS)) |
| 201 | const systemTitleIndex = lines.findIndex((line, index) => ( |
| 202 | index > userTitleIndex && isCaptionPromptSectionTitle(line, CAPTION_PROMPT_SYSTEM_SECTION_TITLE_PATTERNS) |
| 203 | )) |
| 204 | if (userTitleIndex >= 0 && systemTitleIndex > userTitleIndex) |
| 205 | return getCaptionPromptSectionText(lines, userTitleIndex, systemTitleIndex) |
| 206 | |
| 207 | if (userTitleIndex >= 0) |
| 208 | return lines.slice(userTitleIndex + 1).join('\n').trim() |
| 209 | |
| 210 | const systemRequirementPrefixes = [ |
| 211 | options.systemRequirementPrefix?.trim(), |
| 212 | ...CAPTION_PROMPT_SYSTEM_REQUIREMENT_PREFIXES, |
| 213 | ].filter((prefix): prefix is string => Boolean(prefix)) |
| 214 | |
| 215 | const captionPromptWithoutSystemRequirement = stripLeadingSystemRequirementBlock( |
| 216 | normalizedCaptionPrompt, |
| 217 | systemRequirementPrefixes, |
| 218 | ) |
| 219 | if (captionPromptWithoutSystemRequirement !== null) |
| 220 | return captionPromptWithoutSystemRequirement |
| 221 | |
| 222 | return normalizedCaptionPrompt |
| 223 | } |
| 224 | |
| 225 | export function stripDraftPromptLimits(prompt: string) { |
| 226 | const trimmedPrompt = prompt.trimEnd() |
| 227 | const normalizedPrompt = normalizePromptLineBreaks(trimmedPrompt).trim() |
| 228 | const promptWithoutLeadingLimits = stripLeadingSystemRequirementBlock( |
| 229 | normalizedPrompt, |
| 230 | CAPTION_PROMPT_SYSTEM_REQUIREMENT_PREFIXES, |
| 231 | ) |
| 232 | if (promptWithoutLeadingLimits !== null) |
| 233 | return promptWithoutLeadingLimits |
| 234 | |
| 235 | const markerIndex = getLastDraftPromptLimitMarkerIndex(trimmedPrompt) |
| 236 | if (markerIndex < 0) |
| 237 | return prompt |
| 238 | |
| 239 | const markerLength = getDraftPromptLimitMarkerLength(trimmedPrompt, markerIndex) |
| 240 | const limits = trimmedPrompt |
| 241 | .slice(markerIndex + markerLength) |
| 242 | .split(DRAFT_PROMPT_LIMIT_SEPARATOR) |
| 243 | .map(limit => limit.trim()) |
| 244 | .filter(Boolean) |
| 245 | |
| 246 | if (limits.length === 0 || limits.some(limit => !isDraftPromptLimit(limit))) |
| 247 | return prompt |
| 248 | |
| 249 | return trimmedPrompt.slice(0, markerIndex) |
| 250 | } |
| 251 |