返回 oh-my-ppt
prompt-director.ts
根目录 / src / main / image-generation / prompt-director.ts
1 import log from 'electron-log/main.js'
2 import * as cheerio from 'cheerio'
3 import type { ModelRuntimeConfig } from '../agent-runtime/model'
4 import { extractModelText, resolveModel } from '../agent-runtime/model'
5 import { buildImagePromptGenerationMessages } from '../agent-runtime/prompt'
6 import { resolveModelTimeoutMs } from '@shared/model-timeout'
7
8 export type ImagePromptDirectorConfig = {
9 provider: string
10 apiKey: string
11 model: string
12 baseUrl?: string
13 maxTokens: number
14 modelRuntime: ModelRuntimeConfig
15 modelTimeoutMs: unknown
16 locale: 'zh' | 'en'
17 }
18
19 export type ImagePromptDirectorInput = {
20 sessionId: string
21 pageId: string
22 pageTitle: string
23 pageOutline: string
24 pageHtml: string
25 layoutSlotId: string
26 role: string
27 imageGenerationPrompt: string
28 signal?: AbortSignal
29 }
30
31 export const compactPageHtmlForImagePrompt = (html: string): string =>
32 html
33 .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
34 .replace(/<!--[\s\S]*?-->/g, '')
35 .replace(/\s+/g, ' ')
36 .trim()
37 .slice(0, 24000)
38
39 /** The Director needs the slide's meaning, not its full generated CSS and SVG payload. */
40 export const compactPageContentForImageDirector = (html: string): string => {
41 const $ = cheerio.load(html, { scriptingEnabled: false })
42 $('script, style, svg, canvas, template').remove()
43 const content = $('main[data-role="content"]').first()
44 const root = content.length > 0 ? content : $('main').first()
45 const slotLines = root
46 .find('[data-ppt-slot]')
47 .toArray()
48 .map((node) => {
49 const slot = ($(node).attr('data-ppt-slot') || '').trim()
50 const text = $(node).text().replace(/\s+/g, ' ').trim()
51 return slot && text ? `${slot}: ${text}` : text
52 })
53 .filter(Boolean)
54 const fallback = root.text().replace(/\s+/g, ' ').trim()
55 return (slotLines.length > 0 ? slotLines.join('\n') : fallback).slice(0, 6000)
56 }
57
58 export const normalizeGeneratedImagePrompt = (raw: string): string =>
59 raw
60 .replace(/^```(?:text|markdown|md)?\s*/i, '')
61 .replace(/```$/i, '')
62 .replace(/^\s*(?:prompt|提示词)\s*[::]\s*/i, '')
63 .replace(/\s+/g, ' ')
64 .trim()
65
66 const IMAGE_DIRECTOR_CONSTRAINTS =
67 'Return only one concise image-generation prompt. Do not include explanations, JSON, Markdown, logos, watermarks, UI, or remote URLs. Do not invent typography, captions, labels, or lettering-like decoration. Avoid garbled, partial, illegible, or irrelevant text, including pseudo-text and random glyph-like marks. If the image genuinely needs a short text element, state its exact wording in quotation marks, keep it clearly legible and semantically relevant to the page, and do not add any other words.'
68
69 export const createImagePromptDirector = (config: ImagePromptDirectorConfig) =>
70 async (input: ImagePromptDirectorInput): Promise<string> => {
71 const pageContent = compactPageContentForImageDirector(input.pageHtml)
72 const model = resolveModel(
73 config.provider,
74 config.apiKey,
75 config.model,
76 config.baseUrl,
77 0.45,
78 config.maxTokens,
79 config.modelRuntime
80 )
81 const timeoutSignal = AbortSignal.timeout(resolveModelTimeoutMs(config.modelTimeoutMs, 'agent'))
82 const signal = input.signal ? AbortSignal.any([input.signal, timeoutSignal]) : timeoutSignal
83 const userPrompt = [
84 `Design one ${input.role} for the rendered layout slot "${input.layoutSlotId}".`,
85 'The page title, outline, and content define the subject. Turn that meaning into a visual metaphor that supports the factual content instead of repeating it as extra copy or a chart.',
86 `Image style direction (treatment only, not subject matter): ${input.imageGenerationPrompt}`,
87 'Use the style direction for palette, material, atmosphere, and illustration or photography treatment. Do not reuse literal style motifs unless they are semantically relevant to this page. For legal, data, process, or analytical pages, choose a topic-relevant visual metaphor rather than a generic decorative splash.',
88 IMAGE_DIRECTOR_CONSTRAINTS
89 ].join(' ')
90
91 log.info('[images:director] start', {
92 sessionId: input.sessionId,
93 pageId: input.pageId,
94 layoutSlotId: input.layoutSlotId,
95 role: input.role,
96 model: config.model,
97 pageContentLength: pageContent.length
98 })
99 const response = await model.invoke(
100 buildImagePromptGenerationMessages({
101 locale: config.locale,
102 userPrompt,
103 pageTitle: input.pageTitle,
104 pageOutline: input.pageOutline,
105 pageHtml: pageContent
106 }),
107 { signal }
108 )
109 const prompt = normalizeGeneratedImagePrompt(extractModelText(response))
110 if (!prompt) throw new Error('Image director returned an empty prompt.')
111 log.info('[images:director] completed', {
112 sessionId: input.sessionId,
113 pageId: input.pageId,
114 layoutSlotId: input.layoutSlotId,
115 promptLength: prompt.length
116 })
117 return prompt
118 }
119
119 lines TYPESCRIPT