orca/src/shared/native-chat-image-transcrip...

98 lines
3.0 KiB
TypeScript

import { isTextBlock, type NativeChatBlock, type NativeChatMessage } from './native-chat-types'
const IMAGE_SOURCE_MARKER = /^\[Image:\s*source:\s*(.+?)\]\s*$/
const IMAGE_PROMPT_MARKERS = /^(?:\[Image #\d+\]\s*)+/
function soleText(message: NativeChatMessage): string | null {
return message.blocks.length === 1 && isTextBlock(message.blocks[0])
? message.blocks[0].text
: null
}
export function imageSourcePathFromText(text: string): string | null {
return text.match(IMAGE_SOURCE_MARKER)?.[1]?.trim() ?? null
}
export function stripImagePromptMarker(text: string): string {
return text.replace(IMAGE_PROMPT_MARKERS, '')
}
function stripImagePromptMarkersFromFirstText(
blocks: readonly NativeChatBlock[]
): NativeChatBlock[] {
let stripped = false
const next: NativeChatBlock[] = []
for (const block of blocks) {
if (!stripped && isTextBlock(block)) {
stripped = true
const text = stripImagePromptMarker(block.text)
if (text.trim().length > 0) {
next.push({ ...block, text })
}
continue
}
next.push(block)
}
return next
}
function imagePromptMarkerStartsMessage(message: NativeChatMessage): boolean {
const firstText = message.blocks.find(isTextBlock)
return firstText ? IMAGE_PROMPT_MARKERS.test(firstText.text) : false
}
/** Claude records image paths as source turns followed by one marker-prefixed
* prompt. Merge the whole run back into one native user turn. */
export function normalizeImageTranscriptMessages(
messages: readonly NativeChatMessage[]
): NativeChatMessage[] {
const normalized: NativeChatMessage[] = []
for (let index = 0; index < messages.length; index += 1) {
const message = messages[index]!
if (message.role !== 'user') {
normalized.push(message)
continue
}
const imagePath = imageSourcePathFromText(soleText(message) ?? '')
if (imagePath) {
const imagePaths = [imagePath]
let nextIndex = index + 1
while (nextIndex < messages.length) {
const candidate = messages[nextIndex]!
const candidatePath = imageSourcePathFromText(soleText(candidate) ?? '')
if (candidate.role !== 'user' || candidate.source !== message.source || !candidatePath) {
break
}
imagePaths.push(candidatePath)
nextIndex += 1
}
const prompt = messages[nextIndex]
if (
prompt?.role === 'user' &&
prompt.source === message.source &&
imagePromptMarkerStartsMessage(prompt)
) {
normalized.push({
...prompt,
blocks: [
...imagePaths.map((path) => ({ type: 'image-ref' as const, path })),
...stripImagePromptMarkersFromFirstText(prompt.blocks)
]
})
index = nextIndex
continue
}
normalized.push({
...message,
blocks: [{ type: 'image-ref', path: imagePath }]
})
continue
}
normalized.push({
...message,
blocks: stripImagePromptMarkersFromFirstText(message.blocks)
})
}
return normalized
}