rohitsar567 commited on
Commit
f812029
·
verified ·
1 Parent(s): f0b6037

Deploy v1 — single-Docker FastAPI + Next.js + RAG + voice + faithfulness

Browse files
Files changed (3) hide show
  1. backend/main.py +10 -1
  2. backend/persona.py +6 -1
  3. backend/voice_format.py +130 -0
backend/main.py CHANGED
@@ -219,7 +219,16 @@ async def chat(req: ChatRequest):
219
  audio_b64 = None
220
  if req.return_audio and turn.reply_text:
221
  try:
222
- audio = await get_tts().synthesize(turn.reply_text, language_code=req.tts_language_code)
 
 
 
 
 
 
 
 
 
223
  audio_b64 = base64.b64encode(audio).decode("utf-8")
224
  except Exception as e:
225
  # Don't fail the whole turn if TTS hiccups — log + return text only
 
219
  audio_b64 = None
220
  if req.return_audio and turn.reply_text:
221
  try:
222
+ from backend.voice_format import tts_preprocess
223
+ # Send a CLEANED version of the reply to TTS — strip markdown,
224
+ # citations, expand acronyms, truncate. The text in the chat
225
+ # bubble remains the full structured reply.
226
+ spoken = tts_preprocess(
227
+ turn.reply_text,
228
+ language="indic" if req.tts_language_code.startswith("hi") else "en",
229
+ max_words=55,
230
+ )
231
+ audio = await get_tts().synthesize(spoken, language_code=req.tts_language_code)
232
  audio_b64 = base64.b64encode(audio).decode("utf-8")
233
  except Exception as e:
234
  # Don't fail the whole turn if TTS hiccups — log + return text only
backend/persona.py CHANGED
@@ -29,7 +29,12 @@ ABSOLUTE RULES (these are non-negotiable)
29
  When a regulation OVERRIDES a policy clause (e.g., IRDAI mandates 30-day initial waiting period as a minimum), surface both. Regulatory citations are STRONGER signals than policy text — flag them when relevant.
30
  For multi-policy compares, cite each policy separately.
31
 
32
- 3. CONCISE FOR VOICE. Default reply length: under 60 words. Buyers hear this over voice — long replies are unusable. Use bullet points sparingly; prefer short complete sentences.
 
 
 
 
 
33
 
34
  4. NEVER GIVE MEDICAL ADVICE. "Will this be covered if I have X condition?" → answer the COVERAGE question, never the medical one.
35
 
 
29
  When a regulation OVERRIDES a policy clause (e.g., IRDAI mandates 30-day initial waiting period as a minimum), surface both. Regulatory citations are STRONGER signals than policy text — flag them when relevant.
30
  For multi-policy compares, cite each policy separately.
31
 
32
+ 3. CONCISE FOR VOICE — DEFAULT IS SHORT. Most replies should be 2-3 sentences (≤60 words). Buyers hear this over voice — long replies are unusable.
33
+ - Do NOT use markdown bold (`**text**`), italics, or numbered lists in your reply
34
+ - Do NOT use multi-section structures like "Direct answer / Key details / Important note"
35
+ - Use prose sentences, not bullets, unless the user explicitly asks for a list
36
+ - Only go longer (up to 100 words) if the user explicitly asks for "more detail", "full breakdown", or "exclusions list"
37
+ - The text in your reply will be both displayed in chat AND read aloud by TTS — write as if speaking to the user
38
 
39
  4. NEVER GIVE MEDICAL ADVICE. "Will this be covered if I have X condition?" → answer the COVERAGE question, never the medical one.
40
 
backend/voice_format.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Convert an LLM reply (markdown, citations, lists, acronyms) into clean
2
+ spoken-language text for Sarvam Bulbul TTS.
3
+
4
+ Why this exists: an unprocessed LLM reply with markdown bold, inline
5
+ [Source: ...] tags, and acronyms reads like a screenshot when spoken. Users
6
+ hear "asterisk asterisk bold asterisk asterisk A-Y-U-S-H pp dot 1 dash 2".
7
+ That's a UX-killing bug — not a Sarvam limitation, a *us* bug.
8
+
9
+ The function turns text like:
10
+
11
+ "**Direct answer:**
12
+ Yes, HDFC ERGO Optima Secure covers Ayurveda... [Source: my:Optima
13
+ Secure (older variant) (hdfc-ergo), pp.1-2]."
14
+
15
+ Into spoken-ready:
16
+
17
+ "Yes, HDFC ERGO Optima Secure covers Ayurveda treatment at recognized
18
+ Ayush hospitals under specific conditions. For full coverage details
19
+ and exclusions, see the source link below this message."
20
+
21
+ Rules applied (in order):
22
+ 1. Strip [Source: ...] and [Regulation: ...] inline citations
23
+ 2. Strip markdown formatting (** bold, * italic, # headings, > quote, - bullet, 1. number)
24
+ 3. Expand acronyms common in insurance to pronounceable forms
25
+ 4. Compress whitespace
26
+ 5. Truncate to first ~60 spoken words; append "More details on screen." if cut
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import re
32
+
33
+ # ---- markdown / formatting strippers ----
34
+
35
+ CITATION_INLINE = re.compile(r"\s*\[(?:Source|Regulation):[^\]]+\]", flags=re.IGNORECASE)
36
+ MD_BOLD = re.compile(r"\*\*(.+?)\*\*", flags=re.DOTALL)
37
+ MD_ITALIC = re.compile(r"(?<!\*)\*(?!\*)([^*]+?)\*(?!\*)")
38
+ MD_HEADING = re.compile(r"^#{1,6}\s+", flags=re.MULTILINE)
39
+ MD_BLOCKQUOTE = re.compile(r"^>\s+", flags=re.MULTILINE)
40
+ MD_BULLET = re.compile(r"^[\s]*[-•*]\s+", flags=re.MULTILINE)
41
+ MD_NUMBERED = re.compile(r"^\s*\d+\.\s+", flags=re.MULTILINE)
42
+ MD_INLINE_CODE = re.compile(r"`([^`]+)`")
43
+ MD_LINK = re.compile(r"\[([^\]]+)\]\(([^)]+)\)") # keep the text, drop the URL
44
+
45
+ # Acronym expansions — domain-specific so they read naturally aloud.
46
+ # Rule of thumb: if the acronym is normally PRONOUNCED AS A WORD in spoken
47
+ # Indian English (AYUSH, IRDAI, HDFC ERGO), leave it alone; TTS pronounces
48
+ # it fine. If it's normally said as letters (CIS, PED), expand to plain words.
49
+ ACRONYMS = {
50
+ r"\bPED\b": "pre-existing disease",
51
+ r"\bOPD\b": "out-patient",
52
+ r"\bICU\b": "I-C-U",
53
+ r"\bTAT\b": "turnaround time",
54
+ r"\bCSR\b": "claim settlement ratio",
55
+ r"\bNCB\b": "no-claim bonus",
56
+ r"\bSI\b": "sum insured",
57
+ r"\bCIS\b": "Customer Information Sheet",
58
+ r"\bKFD\b": "Key Feature Document",
59
+ r"\bUIN\b": "U-I-N",
60
+ r"\bp\.(\d+)": r"page \1",
61
+ r"\bpp\.(\d+)-(\d+)": r"pages \1 to \2",
62
+ r"\bpp\.(\d+)": r"page \1",
63
+ }
64
+
65
+ # Strip section labels that LLMs love but ruin voice flow.
66
+ # Require the trailing colon so we only catch actual labels, not normal prose
67
+ # that happens to start with "Coverage applies..." etc.
68
+ SECTION_LABELS = re.compile(
69
+ r"^\s*(?:Direct answer|Key details?|Important notes?|Summary|TL;DR|Exclusions? apply|Caveat|Note|Disclaimer)\s*:\s*",
70
+ flags=re.IGNORECASE | re.MULTILINE,
71
+ )
72
+
73
+
74
+ def _strip_markdown(text: str) -> str:
75
+ text = CITATION_INLINE.sub("", text)
76
+ text = MD_BOLD.sub(r"\1", text)
77
+ text = MD_ITALIC.sub(r"\1", text)
78
+ text = MD_HEADING.sub("", text)
79
+ text = MD_BLOCKQUOTE.sub("", text)
80
+ text = MD_BULLET.sub("", text)
81
+ text = MD_NUMBERED.sub("", text)
82
+ text = MD_INLINE_CODE.sub(r"\1", text)
83
+ text = MD_LINK.sub(r"\1", text)
84
+ text = SECTION_LABELS.sub("", text)
85
+ return text
86
+
87
+
88
+ def _expand_acronyms(text: str, language: str = "en") -> str:
89
+ if language == "indic":
90
+ # In Indic mode, keep acronyms — Indic TTS handles them OK
91
+ return text
92
+ for pat, repl in ACRONYMS.items():
93
+ text = re.sub(pat, repl, text, flags=re.IGNORECASE if "ayush" in pat.lower() else 0)
94
+ return text
95
+
96
+
97
+ def _compress_whitespace(text: str) -> str:
98
+ text = re.sub(r"\n{2,}", ". ", text) # paragraph break → sentence break
99
+ text = re.sub(r"\n", " ", text)
100
+ text = re.sub(r"[ \t]{2,}", " ", text)
101
+ # collapse repeated punctuation: ".. ." → "."
102
+ text = re.sub(r"\s*\.\s*\.+", ".", text)
103
+ text = re.sub(r"\s+([.,;:!?])", r"\1", text)
104
+ return text.strip()
105
+
106
+
107
+ def _truncate_for_voice(text: str, max_words: int = 60) -> str:
108
+ """Keep first N words, then append a cutoff cue if we cut anything."""
109
+ words = text.split()
110
+ if len(words) <= max_words:
111
+ return text
112
+ truncated = " ".join(words[:max_words])
113
+ # End on a sentence boundary near the cut
114
+ last_period = max(truncated.rfind("."), truncated.rfind("!"), truncated.rfind("?"))
115
+ if last_period > 0 and last_period > len(truncated) - 80:
116
+ truncated = truncated[: last_period + 1]
117
+ else:
118
+ truncated = truncated + "."
119
+ return truncated + " More details are on screen."
120
+
121
+
122
+ def tts_preprocess(text: str, language: str = "en", max_words: int = 60) -> str:
123
+ """Public entry — turn an LLM reply into spoken-language text for TTS."""
124
+ if not text:
125
+ return ""
126
+ cleaned = _strip_markdown(text)
127
+ cleaned = _expand_acronyms(cleaned, language=language)
128
+ cleaned = _compress_whitespace(cleaned)
129
+ cleaned = _truncate_for_voice(cleaned, max_words=max_words)
130
+ return cleaned