fix(voice): Quote splitting based on single and double quotes

This commit is contained in:
2026-07-19 13:35:19 +05:30
parent a4b620502a
commit 5c3a79e8b6
5 changed files with 90 additions and 5 deletions

View File

@@ -88,7 +88,7 @@
"locationId": "white-room",
"intent": {
"type": "action",
"content": "entity@bf3f29d2-cf11-4b11-9a99-b13c126d400e[I] wake up from my sleep.",
"content": "entity@bf3f29d2-cf11-4b11-9a99-b13c126d400e wakes up from his sleep.",
"actorId": "bf3f29d2-cf11-4b11-9a99-b13c126d400e",
"targetIds": []
}

View File

@@ -1,6 +1,6 @@
import { WorldState, resolveAlias } from "@omnia/core";
import { ILLMProvider } from "@omnia/llm";
import { dehydrate } from "@omnia/voice";
import { dehydrate, expandContractions } from "@omnia/voice";
import { Intent, IntentSequence, LLMIntentSequenceSchema } from "./intent.js";
export class IntentDecoder {
@@ -21,6 +21,7 @@ export class IntentDecoder {
narrativeProse: string,
recentIntents: Intent[] = [],
): Promise<IntentSequence> {
const processedProse = expandContractions(narrativeProse);
const actor = worldState.getEntity(actorId);
// 1. Get other entities co-located in the same context
@@ -75,6 +76,8 @@ For each intent you must:
2. Extract the original narrative text fragment from the prose that corresponds to this intent and populate it as "content". Do not paraphrase, do not convert to third person, and do not convert to second person. Keep the original text fragment exactly as written in the prose (first-person voice).
3. Identify targetIds — the entity IDs of the receiving parties. Use the "Other entities in context" list to resolve any subjective names, aliases, or descriptions used in the prose to their correct entity IDs. If no specific target, use an empty array.
4. Identify modifiers — a list of strings representing additional qualities or modifiers extracted from the narrative prose. This includes emotions, tone of voice, speed, manner of action, or statement type (e.g., "question", "anxious", "whispering", "slowly", "quietly", "forcefully"). If no modifiers are present, use an empty array.
5. For dialogue intents always use the following format for content field:
I say "<dialogue>" (optionally: to him/her/alias).
`.trim();
const userContext = `
@@ -85,7 +88,7 @@ Historical Context:
${historicalContext}
=== NARRATIVE PROSE ===
${narrativeProse}
${processedProse}
`.trim();
const response = await this.llmProvider.generateStructuredResponse({

View File

@@ -0,0 +1,82 @@
import { splitQuotes } from "./dehydration.js";
/**
* Preprocessor that expands common contractions (e.g. "he's" -> "he is", "I'm" -> "I am")
* in non-quote segments of a text, keeping dialogue segments untouched.
*/
export function expandContractions(text: string): string {
if (!text) return "";
const contractionMap: Record<string, string> = {
"i'm": "I am",
"you're": "you are",
"he's": "he is",
"she's": "she is",
"it's": "it is",
"we're": "we are",
"they're": "they are",
"i've": "I have",
"you've": "you have",
"we've": "we have",
"they've": "they have",
"i'd": "I would",
"you'd": "you would",
"he'd": "he would",
"she'd": "she would",
"we'd": "we would",
"they'd": "they would",
"i'll": "I will",
"you'll": "you will",
"he'll": "he will",
"she'll": "she will",
"we'll": "we will",
"they'll": "they will",
"isn't": "is not",
"aren't": "are not",
"wasn't": "was not",
"weren't": "were not",
"haven't": "have not",
"hasn't": "has not",
"hadn't": "had not",
"won't": "will not",
"wouldn't": "would not",
"don't": "do not",
"doesn't": "does not",
"didn't": "did not",
"can't": "cannot",
"couldn't": "could not",
"shouldn't": "should not",
"mightn't": "might not",
"mustn't": "must not",
};
const segments = splitQuotes(text);
const processed = segments.map((seg) => {
if (seg.isQuote) {
return `"${seg.text}"`;
}
let chunk = seg.text;
Object.entries(contractionMap).forEach(([contraction, replacement]) => {
const escaped = contraction.replace("'", "'");
const regex = new RegExp(`\\b${escaped}\\b`, "gi");
chunk = chunk.replace(regex, (matched) => {
const isCapitalized = matched[0] === matched[0].toUpperCase();
let finalRep = replacement;
if (isCapitalized) {
finalRep = finalRep[0].toUpperCase() + finalRep.slice(1);
} else {
if (finalRep.startsWith("I ")) {
// Keep I capitalized
} else {
finalRep = finalRep[0].toLowerCase() + finalRep.slice(1);
}
}
return finalRep;
});
});
return chunk;
});
return processed.join("");
}

View File

@@ -14,13 +14,12 @@ export function splitQuotes(text: string): Segment[] {
for (let i = 0; i < text.length; i++) {
const char = text[i];
if ((char === '"' || char === "'") && (!inQuote || char === quoteChar)) {
if (char === '"') {
if (current) {
segments.push({ text: current, isQuote: inQuote });
current = "";
}
inQuote = !inQuote;
quoteChar = inQuote ? char : "";
} else {
current += char;
}

View File

@@ -1,2 +1,3 @@
export * from "./dehydration.js";
export * from "./hydration.js";
export * from "./contractions.js";