Skip to content

Commit 013e790

Browse files
committed
- Added a secondary pass to clean-up JSON parsing mistakes, improving robustness, in particular with mixed dialogue/narration content.
- Improved error handling when parsing character profiles via Wikia. - Added more loading messages.
1 parent 70a8209 commit 013e790

File tree

3 files changed

+126
-36
lines changed

3 files changed

+126
-36
lines changed

src/components/views/ChatInterface.vue

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,31 +1589,43 @@ const searchForCharactersViaWikiFetch = async (characterNames: string[]): Promis
15891589
{ role: 'user', content: summarizePrompt }
15901590
]
15911591
1592-
try {
1593-
// Call OpenRouter WITHOUT web search - we already have the content
1594-
const result = await callOpenRouter(messages, false)
1595-
1596-
let jsonStr = result.replace(/```json\n?|\n?```/g, '').trim()
1597-
const start = jsonStr.indexOf('{')
1598-
const end = jsonStr.lastIndexOf('}')
1599-
if (start !== -1 && end !== -1) {
1600-
jsonStr = jsonStr.substring(start, end + 1)
1601-
}
1602-
1603-
const profiles = JSON.parse(jsonStr)
1604-
1605-
// Add character IDs
1606-
for (const charName of Object.keys(profiles)) {
1607-
const char = l2d.find((c) => c.name.toLowerCase() === charName.toLowerCase())
1608-
if (char) {
1609-
profiles[charName].id = char.id
1592+
let attempts = 0
1593+
const maxAttempts = 3
1594+
let success = false
1595+
1596+
while (attempts < maxAttempts && !success) {
1597+
try {
1598+
// Call OpenRouter WITHOUT web search - we already have the content
1599+
const result = await callOpenRouter(messages, false)
1600+
1601+
let jsonStr = result.replace(/```json\n?|\n?```/g, '').trim()
1602+
const start = jsonStr.indexOf('{')
1603+
const end = jsonStr.lastIndexOf('}')
1604+
if (start !== -1 && end !== -1) {
1605+
jsonStr = jsonStr.substring(start, end + 1)
1606+
}
1607+
1608+
const profiles = JSON.parse(jsonStr)
1609+
1610+
// Add character IDs
1611+
for (const charName of Object.keys(profiles)) {
1612+
const char = l2d.find((c) => c.name.toLowerCase() === charName.toLowerCase())
1613+
if (char) {
1614+
profiles[charName].id = char.id
1615+
}
1616+
}
1617+
1618+
characterProfiles.value = { ...characterProfiles.value, ...profiles }
1619+
logDebug(`[searchForCharactersViaWikiFetch] Added profile for ${name}:`, profiles)
1620+
success = true
1621+
} catch (e) {
1622+
attempts++
1623+
if (attempts >= maxAttempts) {
1624+
console.error(`[searchForCharactersViaWikiFetch] Failed to process ${name} after ${maxAttempts} attempts:`, e)
1625+
} else {
1626+
console.warn(`[searchForCharactersViaWikiFetch] Attempt ${attempts} failed for ${name}, retrying...`)
16101627
}
16111628
}
1612-
1613-
characterProfiles.value = { ...characterProfiles.value, ...profiles }
1614-
logDebug(`[searchForCharactersViaWikiFetch] Added profile for ${name}:`, profiles)
1615-
} catch (e) {
1616-
console.error(`[searchForCharactersViaWikiFetch] Failed to process ${name}:`, e)
16171629
}
16181630
}
16191631
}
@@ -2236,6 +2248,10 @@ const processAIResponse = async (responseStr: string) => {
22362248
22372249
logDebug('Parsed Action Sequence:', data)
22382250
2251+
// Ensure narration/dialogue separation even when the model returns a single mixed text step.
2252+
data = sanitizeActions(data)
2253+
logDebug('Sanitized Action Sequence:', data)
2254+
22392255
isGenerating.value = false
22402256
loadingStatus.value = "..."
22412257

src/utils/chatUtils.ts

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -187,20 +187,76 @@ export const parseAIResponse = (responseStr: string): any[] => {
187187
export const sanitizeActions = (actions: any[]): any[] => {
188188
const newActions: any[] = []
189189

190+
// Metadata fields that should not be duplicated if we split an action into multiple ones.
191+
// These can trigger side-effects (search, profile updates, debug panels).
192+
const nonDuplicatedFields = new Set([
193+
'needs_search',
194+
'memory',
195+
'characterProgression',
196+
'debug_info'
197+
])
198+
199+
const looksLikeNarrationWithoutQuotes = (rawText: string): boolean => {
200+
const t = (rawText || '').trim()
201+
if (!t) return false
202+
203+
const lower = t.toLowerCase()
204+
205+
for (const char of l2d) {
206+
const name = (char as any).name as string
207+
if (!name) continue
208+
209+
const nameLower = name.toLowerCase()
210+
if (!lower.startsWith(nameLower)) continue
211+
212+
const after = t.slice(name.length)
213+
const nextChar = after.charAt(0)
214+
215+
// Speaker label ("Name:")
216+
if (nextChar === ':') return false
217+
218+
// Direct address ("Name,")
219+
if (nextChar === ',') return false
220+
221+
// Possessive narration: "Name's ..." / "Name’s ..."
222+
if (nextChar === '\'' || nextChar === '’') {
223+
const poss = after.slice(0, 2)
224+
if (poss === "'s" || poss === '’s') return true
225+
}
226+
227+
// Strong narration clue: "Name ..., her/his/ ..." early in the sentence.
228+
if (nextChar && /\s/.test(nextChar)) {
229+
const rest = after.trimStart().slice(0, 80).toLowerCase()
230+
if (rest.includes(', her ') || rest.includes(', his ')) {
231+
return true
232+
}
233+
}
234+
235+
return false
236+
}
237+
238+
return false
239+
}
240+
190241
for (const action of actions) {
191242
if (!action.text || typeof action.text !== 'string') {
192243
newActions.push(action)
193244
continue
194245
}
195246

196-
// Check if the text contains dialogue
247+
// Check if the text contains dialogue.
248+
// Supports straight quotes ("...") and curly quotes (open “ ... close ”).
249+
// Curly quotes are asymmetric, so we match them as a pair explicitly.
197250
const text = action.text
198-
const quotes = '[""“”]'
199-
const dialogueRegex = new RegExp('(' + quotes + ')([^' + quotes + ']*)\\1', 'g')
200-
251+
const dialogueRegex = /("[\s\S]*?"|[\s\S]*?)/g
252+
201253
if (!dialogueRegex.test(text)) {
202-
// No dialogue detected, keep original action
203-
newActions.push(action)
254+
// No quotes. If the model marked it as speaking, keep it UNLESS it looks like third-person narration.
255+
if (action.speaking === true && looksLikeNarrationWithoutQuotes(text)) {
256+
newActions.push({ ...action, speaking: false })
257+
} else {
258+
newActions.push(action)
259+
}
204260
continue
205261
}
206262

@@ -236,19 +292,30 @@ export const sanitizeActions = (actions: any[]): any[] => {
236292
for (let i = 0; i < parts.length; i++) {
237293
const part = parts[i]
238294
if (!part.text.trim()) continue
295+
296+
// Preserve all original fields by default, but avoid duplicating side-effect fields.
297+
const base: any = { ...action }
298+
if (i > 0) {
299+
for (const key of Object.keys(base)) {
300+
if (nonDuplicatedFields.has(key)) {
301+
if (key === 'needs_search') base[key] = []
302+
else delete base[key]
303+
}
304+
}
305+
}
239306

240307
if (part.isDialogue) {
241308
newActions.push({
309+
...base,
242310
text: part.text.trim(),
243-
character: action.character,
244-
animation: action.animation,
245311
speaking: true
246312
})
247313
} else {
314+
// Keep the original character for narration (the narration is *about* that character).
315+
// This prevents narration like from being treated/displayed as spoken dialogue.
248316
newActions.push({
317+
...base,
249318
text: part.text.trim(),
250-
character: 'narrator',
251-
animation: 'idle',
252319
speaking: false
253320
})
254321
}
@@ -265,8 +332,11 @@ export const sanitizeActions = (actions: any[]): any[] => {
265332
// Check if previous is narration, current is dialogue
266333
if (prev.speaking === false &&
267334
curr.speaking === true &&
268-
prev.animation && prev.animation !== 'idle') {
269-
// Copy the narration animation to the dialogue
335+
prev.animation && prev.animation !== 'idle' &&
336+
prev.character && curr.character &&
337+
prev.character === curr.character &&
338+
(!curr.animation || curr.animation === 'idle')) {
339+
// Only carry over animation for the SAME character, and only when the dialogue didn't specify one.
270340
curr.animation = prev.animation
271341
}
272342
}

src/utils/json/loadingMessages.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,9 @@
1313
"Drinking soda with Anis...",
1414
"Plotting the next conspiracy...",
1515
"Cleaning Soda's latest mess...",
16-
"Smashing another window..."
16+
"Smashing another window...",
17+
"Riding the AZX...",
18+
"Receiving funds for the Admire...",
19+
"Exploring the surface...",
20+
"Debating whether coffee is best with milk or sugar..."
1721
]

0 commit comments

Comments
 (0)