1	"""Repository for configurations_v2 table."""
2
3	import json
4	import time
5	from typing import Dict, Any, List
6
7	from loguru import logger
8
9	from src.repository.base import BaseRepository
10
11	# Defaults by region
12	_DEFAULT_LANGUAGES_INDIA = [
13	    {"code": "te", "native_label": "\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41", "script_char": "\u0c24", "english_label": "Telugu"},
14	    {"code": "hi", "native_label": "\u0939\u093f\u0928\u094d\u0926\u0940", "script_char": "\u0939", "english_label": "Hindi"}
15	]
16	_DEFAULT_LANGUAGES_GLOBAL = [
17	    {"code": "en", "native_label": "English", "script_char": "E", "english_label": "English"},
18	    {"code": "es", "native_label": "Español", "script_char": "E", "english_label": "Spanish"},
19	    {"code": "fr", "native_label": "Français", "script_char": "F", "english_label": "French"},
20	    {"code": "pt", "native_label": "Português", "script_char": "P", "english_label": "Portuguese"},
21	    {"code": "id", "native_label": "Bahasa Indonesia", "script_char": "B", "english_label": "Indonesian"},
22	    {"code": "de", "native_label": "Deutsch", "script_char": "D", "english_label": "German"},
23	    {"code": "ja", "native_label": "日本語", "script_char": "日", "english_label": "Japanese"},
24	    {"code": "ar", "native_label": "العربية", "script_char": "ع", "english_label": "Arabic"},
25	    {"code": "ko", "native_label": "한국어", "script_char": "한", "english_label": "Korean"},
26	    {"code": "vi", "native_label": "Tiếng Việt", "script_char": "V", "english_label": "Vietnamese"},
27	]
28
29
30	class ConfigV2Repository(BaseRepository):
31	    """Reads configuration values from the configurations_v2 table.
32
33	    Table schema:
34	        config_key   TEXT PRIMARY KEY
35	        config_value TEXT (JSON string or plain value)
36	        updated_at   INTEGER (unix timestamp)
37	    """
38
39	    async def get_on_open_config(self) -> Dict[str, Any]:
40	        """Get all on_open config values in a single query."""
41	        client = self.get_client()
42	        if not client:
43	            return {}
44
45	        start_time = time.time()
46	        try:
47	            result = await self.query({
48	                "sql": """
49	                    SELECT config_key, config_value
50	                    FROM configurations_v2
51	                    WHERE config_key IN ('update_enabled', 'force_update', 'show_language_screen')
52	                """,
53	                "args": [],
54	            }, "config_v2_on_open")
55
56	            config = {}
57	            if result.rows:
58	                for row in result.rows:
59	                    key = self.extract_text(row, 0)
60	                    value = self.extract_text(row, 1)
61	                    if key and value is not None:
62	                        config[key] = value.lower() in ("true", "1", "yes")
63
64	            duration_ms = (time.time() - start_time) * 1000
65	            logger.debug(f"CONFIG_V2_ON_OPEN: keys={list(config.keys())} duration_ms={duration_ms:.2f}")
66	            return config
67	        except Exception as e:
68	            duration_ms = (time.time() - start_time) * 1000
69	            logger.error(f"CONFIG_V2_ON_OPEN_FAILED: duration_ms={duration_ms:.2f} error={e}")
70	            return {}
71
72	    async def get_languages(self, region: str = None) -> List[Dict[str, Any]]:
73	        """Get supported languages from config, optionally filtered by region.
74
75	        Languages in DB can have a 'region' field: 'india', 'global', or 'all'.
76	        If region is provided, returns only languages matching that region or 'all'.
77	        If no region field exists on a language, it's treated as 'india' for backward compat.
78	        """
79	        from src.core.cache_layer import cached
80
81	        async def _fetch_languages():
82	            try:
83	                result = await self.query({
84	                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
85	                    "args": [{"type": "text", "value": "languages"}],
86	                }, "config_v2_languages")
87	                if result.rows:
88	                    raw = self.extract_text(result.rows[0], 0)
89	                    if raw:
90	                        parsed = json.loads(raw)
91	                        if isinstance(parsed, dict) and "languages" in parsed:
92	                            return parsed
93	                        if isinstance(parsed, list):
94	                            return {"languages": parsed}
95	            except Exception as e:
96	                logger.error(f"CONFIG_V2_LANGUAGES_FETCH_ERROR: {e}")
97	            return None
98
99	        result = await cached(
100	            key="config_v2:languages",
101	            fallback=_fetch_languages,
102	            ttl=300,
103	        )
104
105	        all_languages = []
106	        if result and "languages" in result:
107	            all_languages = result["languages"]
108	        else:
109	            # No DB config — return defaults based on region
110	            if region == "global":
111	                return _DEFAULT_LANGUAGES_GLOBAL
112	            return _DEFAULT_LANGUAGES_INDIA
113
114	        # Filter by region if provided
115	        if region:
116	            filtered = [
117	                lang for lang in all_languages
118	                if lang.get("region", "india") in (region, "all")
119	            ]
120	            return filtered if filtered else all_languages
121
122	        return all_languages
123
124
125	    async def get_language_prompt(self, lang_code: str) -> str:
126	        """Get language instruction for a language code (hi, te)."""
127	        from src.core.cache_layer import cached
128
129	        cache_key = f"config_v2:language_prompt_{lang_code}"
130
131	        async def _fetch():
132	            try:
133	                result = await self.query({
134	                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
135	                    "args": [{"type": "text", "value": f"language_prompt_{lang_code}"}],
136	                }, "config_v2_language_prompt")
137	                if result.rows:
138	                    return self.extract_text(result.rows[0], 0)
139	            except Exception as e:
140	                logger.error(f"CONFIG_V2_LANGUAGE_PROMPT_ERROR: {e}")
141	            return None
142
143	        result = await cached(key=cache_key, fallback=_fetch, ttl=300)
144	        if result:
145	            return result
146	        # Fallback defaults
147	        _FALLBACK_PROMPTS = {
148	            "hi": "Speak in Hinglish — Hindi mixed with English words naturally. Use Roman script.",
149	            "te": "Speak in Telugu using Roman script (Tinglish). Mix English words naturally for tech terms.",
150	            "en": "Speak in English. Keep it casual and conversational.",


Let me continue reading the config_v2_repository and also check routes and any server.py files:


150	            "en": "Speak in English. Keep it casual and conversational.",
151	            "es": "Speak in Spanish. Keep it casual and conversational. Use English words for tech terms.",
152	            "fr": "Speak in French. Keep it casual and conversational. Use English words for tech terms.",
153	            "pt": "Speak in Portuguese. Keep it casual and conversational. Use English words for tech terms.",
154	            "id": "Speak in Indonesian. Keep it casual and conversational. Use English words for tech terms.",
155	            "de": "Speak in German. Keep it casual and conversational. Use English words for tech terms.",
156	            "ja": "Speak in Japanese. Keep it casual and conversational. Use English words for tech terms.",
157	            "ar": "Speak in Arabic. Keep it casual and conversational. Use English words for tech terms.",
158	            "ko": "Speak in Korean. Keep it casual and conversational. Use English words for tech terms.",
159	            "vi": "Speak in Vietnamese. Keep it casual and conversational. Use English words for tech terms.",
160	        }
161	        return _FALLBACK_PROMPTS.get(lang_code, "Speak in English. Keep it casual and conversational.")
162
163	    async def get_firered_config(self) -> Dict[str, Any]:
164	        """Get FireRed image editing config.
165
166	        DB value (JSON): {"enabled": true, "percentage": 20, "max_queue": 3}
167	        - enabled: kill switch
168	        - percentage: % of devices routed to FireRed (SHA256 hash)
169	        - max_queue: if FireRed processing >= this, skip to CF
170	        """
171	        from src.core.cache_layer import cached
172
173	        async def _fetch():
174	            try:
175	                result = await self.query({
176	                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
177	                    "args": [{"type": "text", "value": "firered_config"}],
178	                }, "config_v2_firered")
179	                if result.rows:
180	                    raw = self.extract_text(result.rows[0], 0)
181	                    if raw:
182	                        return json.loads(raw)
183	            except Exception as e:
184	                logger.error(f"CONFIG_V2_FIRERED_ERROR: {e}")
185	            return None
186
187	        result = await cached(key="config_v2:firered_config", fallback=_fetch, ttl=120)
188	        if result:
189	            return result
190	        return {"enabled": False, "percentage": 0, "max_queue": 3}
191
192	    async def get_language_intro(self, lang_code: str, persona_id: str) -> str:
193	        """Get language-specific intro for a persona."""
194	        from src.core.cache_layer import cached
195
196	        async def _fetch():
197	            try:
198	                result = await self.query({
199	                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
200	                    "args": [{"type": "text", "value": "language_intros"}],
201	                }, "config_v2_language_intros")
202	                if result.rows:
203	                    raw = self.extract_text(result.rows[0], 0)
204	                    if raw:
205	                        return json.loads(raw)
206	            except Exception as e:
207	                logger.error(f"CONFIG_V2_LANGUAGE_INTROS_ERROR: {e}")
208	            return None
209
210	        intros = await cached(key="config_v2:language_intros", fallback=_fetch, ttl=300)
211	        if intros and persona_id in intros:
212	            return intros[persona_id].get(lang_code, intros[persona_id].get("hi", ""))
213	        return ""
214
215
216	# Singleton instance
217	config_v2_repo = ConfigV2Repository()
218


Bash

find /home/ubuntu/pipecat -type f -name "server.py" -o -name "app.py" | head -10


/home/ubuntu/pipecat/src/routes/app.py
/home/ubuntu/pipecat/server.py


/home/ubuntu/pipecat/src/repository/config_v2_repository.py-189-            return result
pipecat/src/repository/config_v2_repository.py-190-        return {"enabled": False, "percentage": 0, "max_queue": 3}
/home/ubuntu/pipecat/src/repository/config_v2_repository.py-191-
pipecat/src/repository/config_v2_repository.py:192:    async def get_language_intro(self, lang_code: str, persona_id: str) -> str:
/home/ubuntu/pipecat/src/repository/config_v2_repository.py-193-        """Get language-specific intro for a persona."""
/home/ubuntu/pipecat/src/repository/config_v2_repository.py-194-        from src.core.cache_layer import cached
/home/ubuntu/pipecat/src/repository/config_v2_repository.py-195-
--
pipecat/src/repository/config_v2_repository.py-197-            try:
/home/ubuntu/pipecat/src/repository/config_v2_repository.py-198-                result = await self.query({
pipecat/src/repository/config_v2_repository.py-199-                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
pipecat/src/repository/config_v2_repository.py:200:                    "args": [{"type": "text", "value": "language_intros"}],
pipecat/src/repository/config_v2_repository.py:201:                }, "config_v2_language_intros")
pipecat/src/repository/config_v2_repository.py-202-                if result.rows:
/home/ubuntu/pipecat/src/repository/config_v2_repository.py-203-                    raw = self.extract_text(result.rows[0], 0)
pipecat/src/repository/config_v2_repository.py-204-                    if raw:
--
pipecat/src/repository/config_v2_repository.py-207-                logger.error(f"CONFIG_V2_LANGUAGE_INTROS_ERROR: {e}")
/home/ubuntu/pipecat/src/repository/config_v2_repository.py-208-            return None
/home/ubuntu/pipecat/src/repository/config_v2_repository.py-209-
pipecat/src/repository/config_v2_repository.py:210:        intros = await cached(key="config_v2:language_intros", fallback=_fetch, ttl=300)
pipecat/src/repository/config_v2_repository.py-211-        if intros and persona_id in intros:
/home/ubuntu/pipecat/src/repository/config_v2_repository.py-212-            return intros[persona_id].get(lang_code, intros[persona_id].get("hi", ""))
/home/ubuntu/pipecat/src/repository/config_v2_repository.py-213-        return ""
--
pipecat/src/bot/core/config.py-362-    persona_prompt: Optional[str] = None,
pipecat/src/bot/core/config.py-363-    persona_id: Optional[str] = None,
pipecat/src/bot/core/config.py-364-) -> str:
pipecat/src/bot/core/config.py:365:    """Assemble the system prompt: global + language_instruction + language_intro + personality.
/home/ubuntu/pipecat/src/bot/core/config.py-366-
/home/ubuntu/pipecat/src/bot/core/config.py-367-    Fetches global prompt from DB (character_prompt), language instruction and
/home/ubuntu/pipecat/src/bot/core/config.py-368-    intro from configurations_v2, and personality from persona's system_prompt.
--
/home/ubuntu/pipecat/src/bot/core/config.py-391-    language_instruction = await config_v2_repo.get_language_prompt(lang_code)
/home/ubuntu/pipecat/src/bot/core/config.py-392-
/home/ubuntu/pipecat/src/bot/core/config.py-393-    # 3. Language intro (from configurations_v2, per persona per language)
pipecat/src/bot/core/config.py:394:    language_intro = ""
pipecat/src/bot/core/config.py-395-    if persona_id:
pipecat/src/bot/core/config.py:396:        language_intro = await config_v2_repo.get_language_intro(lang_code, persona_id)
/home/ubuntu/pipecat/src/bot/core/config.py-397-
/home/ubuntu/pipecat/src/bot/core/config.py-398-    # 4. Personality (persona's system_prompt)
/home/ubuntu/pipecat/src/bot/core/config.py-399-    personality = persona_prompt or ""
--
/home/ubuntu/pipecat/src/bot/core/config.py-402-    parts = [global_prompt]
pipecat/src/bot/core/config.py-403-    if language_instruction:
pipecat/src/bot/core/config.py-404-        parts.append(f"LANGUAGE:\n{language_instruction}")
pipecat/src/bot/core/config.py:405:    if language_intro:
pipecat/src/bot/core/config.py:406:        parts.append(f"INTRO (say this when user joins):\n{language_intro}")
pipecat/src/bot/core/config.py-407-    if personality:
pipecat/src/bot/core/config.py-408-        parts.append(f"WHAT YOU CAN HELP WITH:\n{personality}")
/home/ubuntu/pipecat/src/bot/core/config.py-409-
/home/ubuntu/pipecat/src/bot/core/config.py-410-    full_prompt = "\n\n".join(parts)
pipecat/src/bot/core/config.py:411:    logger.info(f"PROMPT_LOADED: persona={persona_id} lang={lang_code} global={len(global_prompt)} lang_inst={len(language_instruction)} intro={len(language_intro)} personality={len(personality)} total={len(full_prompt)}")
/home/ubuntu/pipecat/src/bot/core/config.py-412-    return full_prompt
/home/ubuntu/pipecat/src/bot/core/config.py-413-
/home/ubuntu/pipecat/src/bot/core/config.py-414-


Now let me check the main server.py and routes to see how language is routed:


1	"""Maya Global Agent API Server."""
2
3	import asyncio
4	import time
5	from contextlib import asynccontextmanager
6	from dotenv import load_dotenv
7	from fastapi import FastAPI
8	from fastapi.middleware.cors import CORSMiddleware
9	from loguru import logger
10
11	from src.config import constants
12	from src.core.logging import configure_logging, log_startup_complete, log_shutdown
13	from src.service.config_sync_service import ConfigSyncManager
14	from src.core.redis import init_redis, close_redis
15
16	# Load environment variables
17	load_dotenv(override=True)
18	_startup_time = time.time()
19	configure_logging()
20
21	from src.routes import (
22	    bot_router,
23	    health_router,
24	    video_router,
25	    app_router,
26	    device_router,
27	    support_router,
28	    v2_router,
29	    personas_router,
30	    usage_router,
31	    payment_router,
32	    cleanup_active_sessions,
33	)
34
35	# Initialize config sync manager
36	config_sync_manager = ConfigSyncManager(sync_interval=300)
37
38
39	@asynccontextmanager
40	async def lifespan(app: FastAPI):
41	    """Handles FastAPI startup and shutdown."""
42	    # Initialize Redis cache layer (before config sync so it can use Redis)
43	    await init_redis()
44
45	    await config_sync_manager.start()
46	    startup_duration_ms = (time.time() - _startup_time) * 1000
47	    log_startup_complete(startup_duration_ms)
48	    yield
49	    log_shutdown()
50
51	    # Graceful shutdown: cleanup active bot sessions
52	    await cleanup_active_sessions()
53
54	    await config_sync_manager.stop()
55	    await close_redis()
56	    logger.info("SERVER_STOPPED: status=graceful")
57
58
59	# Create FastAPI app
60	app = FastAPI(
61	    title="Maya Global Agent API",
62	    version="2.0.0",
63	    lifespan=lifespan,
64	    docs_url=None,
65	    redoc_url=None,
66	    openapi_url=None,
67	)
68
69	# Add CORS middleware
70	app.add_middleware(
71	    CORSMiddleware,
72	    allow_origins=["*"],
73	    allow_credentials=True,
74	    allow_methods=["*"],
75	    allow_headers=["*"],
76	)
77
78	# Include routers
79	# Bot connection
80	app.include_router(bot_router, prefix="/api/v4")
81
82	# Health check
83	app.include_router(health_router, prefix="/api/v4")
84
85	# API routes with /api/v4 prefix (prod Kotlin app)
86	app.include_router(video_router, prefix="/api/v4")
87	app.include_router(app_router, prefix="/api/v4")
88	app.include_router(device_router, prefix="/api/v4")
89	app.include_router(support_router, prefix="/api/v4")
90	app.include_router(v2_router, prefix="/api/v4")
91	app.include_router(personas_router, prefix="/api/v4")
92	app.include_router(usage_router, prefix="/api/v4")
93	app.include_router(payment_router, prefix="/api/v4")
94
95	# API routes with /api/v5 prefix (React Native global app)
96	app.include_router(bot_router, prefix="/api/v5")
97	app.include_router(health_router, prefix="/api/v5")
98	app.include_router(video_router, prefix="/api/v5")
99	app.include_router(app_router, prefix="/api/v5")
100	app.include_router(device_router, prefix="/api/v5")
101	app.include_router(support_router, prefix="/api/v5")
102	app.include_router(v2_router, prefix="/api/v5")
103	app.include_router(personas_router, prefix="/api/v5")
104	app.include_router(usage_router, prefix="/api/v5")
105	app.include_router(payment_router, prefix="/api/v5")
106
107
108	async def main():
109	    """Run the FastAPI server."""
110	    import uvicorn
111	    try:
112	        log_config = {
113	            "version": 1,
114	            "disable_existing_loggers": True,
115	            "handlers": {},
116	            "root": {"handlers": [], "level": "INFO"}
117	        }
118	        config = uvicorn.Config(
119	            app,
120	            host=constants.SERVER_HOST,
121	            port=constants.SERVER_PORT,
122	            log_config=log_config
123	        )
124	        server = uvicorn.Server(config)
125	        await server.serve()
126	    except asyncio.CancelledError:
127	        logger.info("Server cancelled (shutdown).")
128
129
130	if __name__ == "__main__":
131	    asyncio.run(main())
132


1	"""Bot connection API endpoints (SmallWebRTC + Daily dual transport)."""
2
3	import asyncio
4	import os
5	import uuid
6	from typing import Any, Dict, List, Optional
7
8	import aiohttp
9	from fastapi import APIRouter, HTTPException, Request, UploadFile, File, Form
10	from loguru import logger
11	from pydantic import BaseModel
12
13	from pipecat.transports.smallwebrtc.connection import IceServer
14	from pipecat.transports.smallwebrtc.request_handler import (
15	    SmallWebRTCRequestHandler,
16	    SmallWebRTCRequest,
17	    SmallWebRTCPatchRequest,
18	    IceCandidate,
19	)
20
21	from src.bot import run_bot_webrtc_global_agent
22	from src.config import constants
23	from src.core.logging import log_error
24	from src.core.redis import (
25	    add_active_session, remove_active_session, cleanup_pod_sessions,
26	    redis_available, _get_pod_name,
27	    set_session_pod, get_session_pod, delete_session_pod,
28	)
29	from src.core.pod_lifecycle import on_session_complete
30	from src.dto.common import BotConnectRequest
31
32
33	def _get_ice_servers() -> list[IceServer]:
34	    """Build ICE server list from config. TURN is required for cloud/NAT traversal."""
35	    servers = [IceServer(urls="stun:bn-turn1.xirsys.com")]
36	    if constants.TURN_USERNAME and constants.TURN_PASSWORD:
37	        turn_urls = [
38	            "turn:bn-turn1.xirsys.com:80?transport=udp",
39	            "turn:bn-turn1.xirsys.com:3478?transport=udp",
40	            "turn:bn-turn1.xirsys.com:80?transport=tcp",
41	            "turn:bn-turn1.xirsys.com:3478?transport=tcp",
42	            "turns:bn-turn1.xirsys.com:443?transport=tcp",
43	            "turns:bn-turn1.xirsys.com:5349?transport=tcp",
44	        ]
45	        servers.append(IceServer(
46	            urls=turn_urls,
47	            username=constants.TURN_USERNAME,
48	            credential=constants.TURN_PASSWORD,
49	        ))
50	        logger.info("TURN_SERVER_CONFIGURED: xirsys")
51	    else:
52	        logger.warning("NO_TURN_SERVER: WebRTC will fail behind NAT/K8s without TURN")
53	    return servers
54
55
56	def _get_transport_mode() -> str:
57	    """Get current transport mode from DB config (synced periodically)."""
58	    from src.repository.config_repository import config_repo
59	    return config_repo.get_transport_mode()
60
61
62	router = APIRouter(tags=["bot"])
63
64	# Port the app listens on inside each pod
65	_POD_PORT = 3008
66
67
68	async def _proxy_to_pod(pod_ip: str, method: str, path: str, body: dict = None) -> dict:
69	    """Proxy a request to another pod internally (pod-to-pod within cluster)."""
70	    url = f"http://{pod_ip}:{_POD_PORT}{path}"
71	    logger.info(f"PROXY_REQUEST: method={method} url={url}")
72	    try:
73	        async with aiohttp.ClientSession() as session:
74	            if method == "POST":
75	                async with session.post(url, json=body, timeout=aiohttp.ClientTimeout(total=15)) as resp:
76	                    if resp.status >= 400:
77	                        text = await resp.text()
78	                        raise HTTPException(status_code=resp.status, detail=text)
79	                    return await resp.json()
80	            elif method == "PATCH":
81	                async with session.patch(url, json=body, timeout=aiohttp.ClientTimeout(total=15)) as resp:
82	                    if resp.status >= 400:
83	                        text = await resp.text()
84	                        raise HTTPException(status_code=resp.status, detail=text)
85	                    return await resp.json()
86	    except HTTPException:
87	        raise
88	    except Exception as e:
89	        logger.error(f"PROXY_FAILED: url={url} error={e}")
90	        raise HTTPException(status_code=502, detail=f"Proxy to session pod failed: {e}")
91
92	# Track active sessions: {task: session_id}
93	active_sessions: dict[asyncio.Task, str] = {}
94
95	# SmallWebRTC request handlers per session: {session_id: handler}
96	session_handlers: dict[str, SmallWebRTCRequestHandler] = {}
97
98	# Session params stored between /connect and /offer: {session_id: params_dict}
99	session_params: dict[str, dict] = {}
100


Found 9 files
pipecat/src/routes/referral.py
pipecat/src/routes/payment.py
pipecat/src/routes/app.py
pipecat/src/routes/usage.py
pipecat/src/routes/v2.py
pipecat/src/routes/bot.py
pipecat/src/routes/video.py
pipecat/src/routes/device.py
pipecat/src/routes/support.py


Let me search for language routing in the routes and check device repository:


grep -r "language" /home/ubuntu/pipecat/src/routes/*.py --include="*.py" -n | head -40


/home/ubuntu/pipecat/src/routes/bot.py:191:    language = data.language
/home/ubuntu/pipecat/src/routes/bot.py:198:        logger.info(f"SESSION_START: device_id={device_id or 'common'} version={version} persona_id={persona_id or 'default'} app_key={app_key or 'legacy'} language={language or 'default'} source={source or 'app'} app_version={app_version} transport_mode={transport_mode}")
/home/ubuntu/pipecat/src/routes/bot.py:207:            "language": language,
/home/ubuntu/pipecat/src/routes/bot.py:263:        language=params.get("language"),
/home/ubuntu/pipecat/src/routes/bot.py:321:            language=params.get("language"),
/home/ubuntu/pipecat/src/routes/v2.py:6:from src.service.app_v2_service import get_on_open_config, set_device_language
/home/ubuntu/pipecat/src/routes/v2.py:25:@router.post("/set_language")
/home/ubuntu/pipecat/src/routes/v2.py:26:async def set_language(device_id: str, language: str):
/home/ubuntu/pipecat/src/routes/v2.py:27:    """Called after user selects language on onboarding. Marks device as onboarded."""
/home/ubuntu/pipecat/src/routes/v2.py:29:        result = await set_device_language(device_id=device_id, language=language)
/home/ubuntu/pipecat/src/routes/v2.py:32:        logger.error(f"SET_LANGUAGE_ERROR: device_id={device_id} language={language} error={e}")
/home/ubuntu/pipecat/src/routes/v2.py:38:    """Called when the app opens. Returns config flags, languages, and region.
/home/ubuntu/pipecat/src/routes/v2.py:42:    and returns languages filtered by region.
/home/ubuntu/pipecat/src/routes/v2.py:51:            "languages": [],


Let me read the device_repository and the v2 routes more carefully:


1	"""Repository for device-related DB operations."""
2
3	import time
4	from datetime import datetime, timezone
5	from typing import Dict, Any, Optional
6
7	import pytz
8	from loguru import logger
9
10	from src.repository.base import BaseRepository
11
12	# Build timezone → country code map from pytz (at import time, never changes)
13	_TZ_TO_COUNTRY: Dict[str, str] = {}
14	for _cc, _tzs in pytz.country_timezones.items():
15	    for _tz in _tzs:
16	        _TZ_TO_COUNTRY[_tz] = _cc
17
18
19	def determine_region(tz: str) -> str:
20	    """Map timezone to region. Asia/Kolkata and Asia/Calcutta = india, everything else = global."""
21	    if tz in ("Asia/Kolkata", "Asia/Calcutta"):
22	        return "india"
23	    return "global"
24
25
26	def determine_country(tz: str) -> Optional[str]:
27	    """Map timezone to ISO 3166-1 alpha-2 country code using pytz. Returns None if unknown."""
28	    return _TZ_TO_COUNTRY.get(tz)
29
30
31	class DeviceRepository(BaseRepository):
32	    """Handles device table reads/writes, premium checks, limits, and review eligibility."""
33
34	    async def save_notification_token(self, device_id: str, notification_id: str) -> bool:
35	        client = self.get_client()
36	        if client is None:
37	            return False
38
39	        start_time = time.time()
40	        try:
41	            check_result = await self.query({
42	                "sql": "SELECT expo_notification_id FROM devices WHERE device_id = ? LIMIT 1",
43	                "args": [device_id]
44	            }, "check_notification_token")
45
46	            if check_result.rows and len(check_result.rows) > 0:
47	                existing_token = self.extract_text(check_result.rows[0], 0)
48	                if existing_token == notification_id:
49	                    return True
50
51	            await self.query_with_retry({
52	                "sql": """
53	                    INSERT INTO devices (device_id, expo_notification_id, created_at, updated_at)
54	                    VALUES (?, ?, unixepoch(), unixepoch())
55	                    ON CONFLICT(device_id) DO UPDATE SET
56	                        expo_notification_id = excluded.expo_notification_id,
57	                        updated_at = unixepoch()
58	                """,
59	                "args": [device_id, notification_id]
60	            }, "save_notification_token")
61
62	            duration_ms = (time.time() - start_time) * 1000
63	            logger.info(f"DB_NOTIFICATION_TOKEN_SAVED: device_id={device_id} duration_ms={duration_ms:.2f}")
64	            return True
65	        except Exception as e:
66	            duration_ms = (time.time() - start_time) * 1000
67	            logger.error(f"ERROR: DB_NOTIFICATION_TOKEN_SAVE_FAILED device_id={device_id} duration_ms={duration_ms:.2f} error={str(e)}")
68	            return False
69
70	    # ---- App Review ----
71	    async def _should_show_app_review_db(self, device_id: str) -> bool:
72	        client = self.get_client()
73	        if not client:
74	            return False
75
76	        total_start = time.time()
77	        try:
78	            from src.repository.config_repository import config_repo
79	            config = await config_repo.get_app_review_config()
80	            sessions_required = config.get("sessions_required", 5)
81	            generations_required = config.get("generations_required", 10)
82	            days_between = config.get("days_between_reviews", 7)
83
84	            result = await self.query({
85	                "sql": "SELECT last_review_at FROM devices WHERE device_id = ? LIMIT 1",
86	                "args": [device_id]
87	            }, "should_show_app_review_get_last")
88
89	            last_review_at = None
90	            if result.rows and len(result.rows) > 0:
91	                row = result.rows[0]
92	                if isinstance(row, list) and len(row) > 0:
93	                    review_dict = row[0] if isinstance(row[0], dict) else None
94	                    review_val = review_dict.get("value") if review_dict and review_dict.get("type") == "integer" else None
95	                    last_review_at = int(review_val) if review_val is not None and review_val != 0 else None
96	                elif isinstance(row, dict):
97	                    last_review_val = row.get("last_review_at")
98	                    last_review_at = int(last_review_val) if last_review_val and last_review_val != 0 else None
99
100	            if last_review_at and last_review_at > 0:
101	                last_review_date = datetime.fromtimestamp(last_review_at, tz=timezone.utc)
102	                days_since_review = (datetime.now(timezone.utc) - last_review_date).days
103	                if days_since_review < days_between:
104	                    return False
105
106	            session_result = await self.query({
107	                "sql": "SELECT COUNT(*) as session_count FROM sessions WHERE device_id = ? AND status = 'completed'",
108	                "args": [device_id]
109	            }, "should_show_app_review_sessions")
110
111	            session_count = 0
112	            if session_result.rows and len(session_result.rows) > 0:
113	                row = session_result.rows[0]
114	                if isinstance(row, list) and len(row) > 0:
115	                    count_dict = row[0] if isinstance(row[0], dict) else None
116	                    session_count = int(count_dict.get("value", 0)) if count_dict and count_dict.get("type") == "integer" else 0
117	                elif isinstance(row, dict):
118	                    session_count = int(row.get("session_count", 0))
119
120	            generation_result = await self.query({
121	                "sql": """
122	                    SELECT COUNT(*) as gen_count
123	                    FROM sessions s,
124	                    json_each(s.generations) as gen
125	                    WHERE s.device_id = ?
126	                      AND s.generations IS NOT NULL
127	                      AND json_valid(s.generations)
128	                      AND json_extract(gen.value, '$.generation_type') IN ('image', 'image_edit', 'image_analysis')
129	                """,
130	                "args": [device_id]
131	            }, "should_show_app_review_generations")
132
133	            generation_count = 0
134	            if generation_result.rows and len(generation_result.rows) > 0:
135	                row = generation_result.rows[0]
136	                if isinstance(row, list) and len(row) > 0:
137	                    count_dict = row[0] if isinstance(row[0], dict) else None
138	                    generation_count = int(count_dict.get("value", 0)) if count_dict and count_dict.get("type") == "integer" else 0
139	                elif isinstance(row, dict):
140	                    generation_count = int(row.get("gen_count", 0))
141
142	            should_show = (session_count >= sessions_required) or (generation_count >= generations_required)
143
144	            total_duration_ms = (time.time() - total_start) * 1000
145	            logger.info(f"DB_REVIEW_ELIGIBILITY: device_id={device_id} sessions={session_count} generations={generation_count} should_show={should_show} total_duration_ms={total_duration_ms:.2f} queries=4")
146	            return should_show
147	        except Exception as e:
148	            total_duration_ms = (time.time() - total_start) * 1000
149	            logger.error(f"ERROR: DB_REVIEW_ELIGIBILITY_FAILED device_id={device_id} total_duration_ms={total_duration_ms:.2f} error={str(e)}")
150	            return False


1	"""V2 API endpoints."""
2
3	from fastapi import APIRouter
4	from loguru import logger
5
6	from src.service.app_v2_service import get_on_open_config, set_device_language
7	from src.service.usage_service import can_generate_persona
8
9	router = APIRouter(tags=["v3"])
10
11
12	@router.get("/check-limits")
13	async def check_limits(device_id: str, persona_id: str):
14	    """Pre-check if device can use a persona. Returns plan + topup data when blocked."""
15	    try:
16	        result = await can_generate_persona(device_id, persona_id)
17	        if not result["allowed"]:
18	            logger.info(f"CHECK_LIMITS_BLOCKED: device_id={device_id} persona={persona_id} reason={result['reason']}")
19	        return result
20	    except Exception as e:
21	        logger.error(f"CHECK_LIMITS_ERROR: device_id={device_id} persona={persona_id} error={e}")
22	        return {"allowed": True}  # fail open
23
24
25	@router.post("/set_language")
26	async def set_language(device_id: str, language: str):
27	    """Called after user selects language on onboarding. Marks device as onboarded."""
28	    try:
29	        result = await set_device_language(device_id=device_id, language=language)
30	        return result
31	    except Exception as e:
32	        logger.error(f"SET_LANGUAGE_ERROR: device_id={device_id} language={language} error={e}")
33	        return {"success": False, "error": str(e)}
34
35
36	@router.get("/on_open")
37	async def on_open(device_id: str = None, timezone: str = None):
38	    """Called when the app opens. Returns config flags, languages, and region.
39
40	    On first launch the app sends the device timezone (e.g. 'Asia/Kolkata').
41	    The backend determines the region (india/global), stores it on the device,
42	    and returns languages filtered by region.
43	    """
44	    try:
45	        return await get_on_open_config(device_id=device_id, timezone=timezone)
46	    except Exception as e:
47	        logger.error(f"V2_ON_OPEN_ERROR: device_id={device_id} timezone={timezone} error={e}")
48	        return {
49	            "update_enabled": False,
50	            "force_update": False,
51	            "languages": [],
52	            "region": "india",
53	        }
54


150	        # Cleanup handler and params
151	        session_handlers.pop(sid, None)
152	        session_params.pop(sid, None)
153	        if sid and redis_available():
154	            try:
155	                loop = asyncio.get_event_loop()
156	                if loop.is_running():
157	                    asyncio.create_task(remove_active_session(sid))
158	                    asyncio.create_task(delete_session_pod(sid))
159	                    logger.info(f"REDIS_CLEANUP_SCHEDULED: session={sid}")
160	                else:
161	                    logger.warning(f"REDIS_CLEANUP_SKIPPED: session={sid} reason=loop_not_running")
162	            except RuntimeError:
163	                logger.warning(f"REDIS_CLEANUP_SKIPPED: session={sid} reason=no_event_loop")
164	            except Exception as e:
165	                logger.error(f"REDIS_CLEANUP_CALLBACK_ERROR: session={sid} error={e}")
166
167	    task.add_done_callback(cleanup_session)
168
169
170	@router.get("/transport-type")
171	async def get_transport_type():
172	    """Lightweight endpoint — returns current transport mode without creating a session."""
173	    mode = _get_transport_mode()
174	    logger.info(f"TRANSPORT_TYPE_CHECK: mode={mode}")
175	    return {"transport": mode}
176
177
178	@router.post("/start")
179	async def bot_connect(data: BotConnectRequest) -> Dict[Any, Any]:
180	    """Unified connection endpoint — returns transport-specific response.
181
182	    Transport mode is controlled by DB config (transport_config.mode).
183	    - SmallWebRTC: returns {"transport": "smallwebrtc", "sessionId": "..."}
184	    - Daily: returns {"transport": "daily", "room_url": "...", "token": "..."}
185	    """
186	    logger.info(f"CONNECT_INPUT_BODY: {data.model_dump()}")
187	    device_id = data.device_id
188	    version = data.version
189	    persona_id = data.persona_id or data.character_id
190	    app_key = data.app_key
191	    language = data.language
192	    source = data.source
193	    app_version = data.app_version
194
195	    transport_mode = _get_transport_mode()
196
197	    try:
198	        logger.info(f"SESSION_START: device_id={device_id or 'common'} version={version} persona_id={persona_id or 'default'} app_key={app_key or 'legacy'} language={language or 'default'} source={source or 'app'} app_version={app_version} transport_mode={transport_mode}")
199
200	        session_id = str(uuid.uuid4())
201
202	        params = {
203	            "device_id": device_id,
204	            "version": version,
205	            "persona_id": persona_id,
206	            "app_key": app_key,
207	            "language": language,
208	            "source": source,
209	            "app_version": app_version,
210	        }
211
212	        if transport_mode == "daily":
213	            return await _start_daily_session(session_id, params)
214	        else:
215	            return await _start_smallwebrtc_session(session_id, params)
216
217	    except Exception as e:
218	        log_error("SESSION_START_FAILED", e, device_id=device_id or "common", transport=transport_mode)
219	        raise HTTPException(status_code=500, detail=str(e))
220
221
222	async def _start_smallwebrtc_session(session_id: str, params: dict) -> dict:
223	    """SmallWebRTC flow: return sessionId, client posts SDP offer next."""
224	    handler = SmallWebRTCRequestHandler(ice_servers=_get_ice_servers())
225	    session_handlers[session_id] = handler
226	    params["transport"] = "smallwebrtc"
227	    session_params[session_id] = params
228
229	    logger.info(f"SESSION_CREATED: session_id={session_id} transport=smallwebrtc device_id={params.get('device_id') or 'common'}")
230
231	    # Store session → pod_ip mapping in Redis for cross-pod routing
232	    if redis_available():
233	        await set_session_pod(session_id)
234
235	    # Return ICE servers so client can configure RTCPeerConnection with TURN
236	    ice_servers_response = []
237	    for server in _get_ice_servers():
238	        entry = {"urls": server.urls if isinstance(server.urls, list) else [server.urls]}
239	        if server.username:
240	            entry["username"] = server.username
241	        if server.credential:
242	            entry["credential"] = server.credential
243	        ice_servers_response.append(entry)
244
245	    return {"transport": "smallwebrtc", "sessionId": session_id, "iceConfig": {"iceServers": ice_servers_response}}
246
247
248	async def _start_daily_session(session_id: str, params: dict) -> dict:
249	    """Daily flow: create room + token, start bot immediately, return credentials."""


Perfect! Now let me check the app_v2_service to see the set_device_language and get_on_open_config functions:


find /home/ubuntu/pipecat -name "app_v2_service.py" -type f


1	"""Service layer for v2 app endpoints."""
2
3	from typing import Optional, Dict, Any
4
5	from loguru import logger
6
7	from src.repository.config_v2_repository import config_v2_repo
8	from src.repository.device_repository import device_repo, determine_region, determine_country
9	from src.repository.subscription_repository import subscription_repo
10
11
12	async def set_device_language(*, device_id: str, language: str) -> dict:
13	    """Called after user picks language on onboarding screen. Marks device onboarded."""
14	    device = await device_repo.get_device(device_id)
15	    region = (device.get("region") if device else None) or "india"
16	    country = device.get("country") if device else None
17
18	    success = await device_repo.onboard_device(device_id, region, language, country)
19	    if success:
20	        logger.info(f"DEVICE_ONBOARDED: device_id={device_id} region={region} language={language} country={country}")
21	        return {"success": True, "region": region, "language": language}
22	    return {"success": False, "error": "Failed to save language"}
23
24
25	async def get_on_open_config(*, device_id: Optional[str] = None, timezone: Optional[str] = None) -> dict:
26	    """Get the v2 on_open response.
27
28	    On first launch (device not onboarded), uses timezone to determine region
29	    and stores it on the device record. Region is locked after onboarding.
30
31	    Returns config flags, languages filtered by region, and the region itself.
32	    """
33	    region = "india"  # default for backward compat (existing devices with NULL region)
34
35	    # Determine / fetch region for this device
36	    show_language_screen = False
37	    device_language = None
38
39	    if device_id:
40	        device = await device_repo.get_device(device_id)
41
42	        if device and device.get("onboarded"):
43	            # Already onboarded — use stored region and language
44	            region = device.get("region") or "india"
45	            device_language = device.get("language")
46	            # Backfill country for existing users who onboarded before country was tracked
47	            if not device.get("country") and timezone:
48	                country = determine_country(timezone)
49	                if country:
50	                    await device_repo.save_country(device_id, country)
51	                    logger.info(f"DEVICE_COUNTRY_BACKFILL: device_id={device_id} timezone={timezone} country={country}")
52	        elif device:
53	            # Device exists but not onboarded
54	            # Use stored region if available, otherwise determine from timezone
55	            stored_region = device.get("region")
56	            if stored_region:
57	                region = stored_region
58	            elif timezone:
59	                region = determine_region(timezone)
60	                country = determine_country(timezone)
61	                await device_repo.save_region(device_id, region, country)
62	                logger.info(f"DEVICE_REGION_SET: device_id={device_id} timezone={timezone} region={region} country={country}")
63	            show_language_screen = True
64	        elif timezone:
65	            # Brand new device — determine region from timezone
66	            region = determine_region(timezone)
67	            country = determine_country(timezone)
68	            await device_repo.save_region(device_id, region, country)
69	            show_language_screen = True
70	            logger.info(f"DEVICE_REGION_SET: device_id={device_id} timezone={timezone} region={region} country={country}")
71	        else:
72	            # No device, no timezone — show language screen
73	            show_language_screen = True
74
75	    config = await config_v2_repo.get_on_open_config()
76	    languages = await config_v2_repo.get_languages(region=region)
77
78	    # DB config can force language screen only for non-onboarded users
79	    # Once onboarded, never show language screen on open
80	    if not show_language_screen and not device_language and config.get("show_language_screen", False):
81	        show_language_screen = True
82
83	    # Check premium status — only for global users (India doesn't show premium button)
84	    is_premium = False
85	    if device_id and region != "india":
86	        for ct in ["image_gen", "video_gen", "ad_video", "ppt_gen", "music_gen", "all"]:
87	            sub = await subscription_repo.get_active(device_id, ct, "google_play")
88	            if sub:
89	                is_premium = True
90	                break
91
92	    response: Dict[str, Any] = {
93	        "update_enabled": config.get("update_enabled", False),
94	        "force_update": config.get("force_update", False),
95	        "show_language_screen": show_language_screen,
96	        "languages": languages,
97	        "region": region,
98	        "language": device_language,
99	        "is_premium": is_premium,
100	    }
101
102	    return response
103


Now let me check where language intros and device language are used in the database setup:


grep -r "Language\." /home/ubuntu/pipecat/src/bot/core/pipeline.py | head -20


            language=Language.HI,
            language_hints=[Language.TE],
        "te": Language.TE_IN,
        "hi": Language.HI_IN,
        "en": Language.EN_US,
        "es": Language.ES_ES,
        "fr": Language.FR_FR,
        "pt": Language.PT_BR,
        "id": Language.ID_ID,
        "de": Language.DE_DE,
        "ja": Language.JA_JP,
        "ar": Language.AR_SA,
        "ko": Language.KO_KR,
        "vi": Language.VI_VN,
    gemini_language = _LANG_MAP.get(language, Language.EN_US)


Perfect! Now let me create a comprehensive summary. Let me also check config_repository for language-related methods:


grep -n "language\|hindi" /home/ubuntu/pipecat/src/repository/config_repository.py -i | head -50


471:    async def get_active_prompt(self, prompt_name: str = "hindi_prompt") -> Optional[str]:


Perfect! Now I have enough information. Let me create a comprehensive report:

## Summary: Hindi Language Handling in Pipecat Codebase

Based on my thorough analysis of the pipecat codebase, here's how Hindi language users are handled:

### 1. **Language-Based Routing & Pipeline Selection**

**File: `/home/ubuntu/pipecat/src/bot/core/main.py` (Lines 23, 38, 62-68, 74-77, 161, 167)**

The bot accepts `language` as a parameter and uses it for routing:
```python
async def run_bot_webrtc_global_agent(
    ...,
    language: str = None,  # Language passed from client
    ...
):
    bot_session.language = language

    # DB-based language override: Fetch device info early—use DB language instead of frontend-sent value
    if device_id != "common":
        device_info = await device_repo.get_device(device_id)
        if device_info and device_info.get("language"):
            language = device_info["language"]  # DB takes precedence
            bot_session.language = language
            logger.info(f"LANGUAGE_FROM_DB: device={device_id} language={language}")
```

**Key routing decision: DB-stored language overrides frontend language parameter.**

### 2. **Gemini Live Service & Language Mapping**

**File: `/home/ubuntu/pipecat/src/bot/core/pipeline.py` (Lines 533-606)**

Hindi language is explicitly mapped to Gemini Live service:
```python
def create_gemini_live_service(system_instruction: str, voice_id: str = None, ..., language: str = None):
    """Maps language codes to Gemini Live Language enum"""
    from pipecat.transcriptions.language import Language

    _LANG_MAP = {
        "te": Language.TE_IN,
        "hi": Language.HI_IN,  # Hindi (India)
        "en": Language.EN_US,
        "es": Language.ES_ES,
        "fr": Language.FR_FR,
        "pt": Language.PT_BR,
        "id": Language.ID_ID,
        "de": Language.DE_DE,
        "ja": Language.JA_JP,
        "ar": Language.AR_SA,
        "ko": Language.KO_KR,
        "vi": Language.VI_VN,
    }
    gemini_language = _LANG_MAP.get(language, Language.EN_US)

    llm = GeminiLiveVertexLLMService(
        settings=GeminiLiveVertexLLMService.Settings(
            voice=voice,
            language=gemini_language,  # Sets STT/TTS language
        ),
    )
```

**Hindi users get `Language.HI_IN` for both STT and TTS in Gemini Live pipeline.**

### 3. **TTS/STT Model Selection**

**File: `/home/ubuntu/pipecat/src/config/constants.py` (Lines 82-83)**

Default STT configuration:
```python
STT_MODEL = "gpt-4o-mini-transcribe-2025-12-15"
STT_LANGUAGE = "hi"  # Default to Hindi
```

**File: `/home/ubuntu/pipecat/src/bot/core/pipeline.py` (Lines 130-153, 391-427)**

For mobile (non-Gemini Live) pipelines:
```python
def _create_cf_nova3_stt_service():
    """CF Workers AI Deepgram Nova-3 STT service (Hindi-optimized)"""
    stt = DeepgramSTTService(
        settings=DeepgramSTTService.Settings(
            model="nova-3-general",
            language=Language.HI,  # Explicitly set to Hindi
            interim_results=True,
            punctuate=True,
            ...
        ),
    )
```

**TTS Service:**
```python
def create_gemini_tts_service(voice: str = None):
    """Uses Google Chirp3 HD for Telugu"""
    tts = GoogleTTSService(
        settings=GoogleTTSService.Settings(
            voice="te-IN-Chirp3-HD-Erinome",
            language="te-IN",
        ),
    )
```

### 4. **Config & DB-Based Language Routing**

**File: `/home/ubuntu/pipecat/src/repository/config_v2_repository.py` (Lines 12-27, 72-122, 125-161, 192-213)**

Languages are managed in DB and filtered by region:
```python
_DEFAULT_LANGUAGES_INDIA = [
    {"code": "te", "native_label": "తెలుగు", "script_char": "త", "english_label": "Telugu"},
    {"code": "hi", "native_label": "हिन्दी", "script_char": "ह", "english_label": "Hindi"}
]

async def get_languages(self, region: str = None) -> List[Dict[str, Any]]:
    """Get supported languages from config, optionally filtered by region."""
    # Returns Hindi for India region
```

Language-specific prompts stored in `configurations_v2`:
```python
async def get_language_prompt(self, lang_code: str) -> str:
    """Get language instruction for language code (hi, te)"""
    # Fetches cached language instructions like:
    _FALLBACK_PROMPTS = {
        "hi": "Speak in Hinglish — Hindi mixed with English words naturally. Use Roman script.",
        ...
    }
```

**Per-persona Hindi intros:**
```python
async def get_language_intro(self, lang_code: str, persona_id: str) -> str:
    """Get language-specific intro for a persona from configurations_v2['language_intros']"""
    # Looks up: language_intros[persona_id]["hi"] with fallback to "hi"
```

### 5. **System Prompt Assembly**

**File: `/home/ubuntu/pipecat/src/bot/core/config.py` (Lines 360-412)**

Full system prompt is assembled with language-specific instructions:
```python
async def load_system_instruction(
    language: Optional[str] = None,
    persona_prompt: Optional[str] = None,
    persona_id: Optional[str] = None,
) -> str:
    lang_code = language or "hi"  # Hindi is default!

    # 1. Global prompt from DB or file
    # 2. Language instruction: await config_v2_repo.get_language_prompt(lang_code)
    # 3. Language intro (per persona): await config_v2_repo.get_language_intro(lang_code, persona_id)
    # 4. Personality: persona's system_prompt

    # Assemble: global + language + intro + personality
    parts = [global_prompt]
    if language_instruction:
        parts.append(f"LANGUAGE:\n{language_instruction}")
    if language_intro:
        parts.append(f"INTRO (say this when user joins):\n{language_intro}")
```

### 6. **Language-Based Error Messages**

**File: `/home/ubuntu/pipecat/src/config/constants.py` (Lines 161-185)**

Hindi-specific error messages:
```python
ERROR_NO_IMAGE_FOR_EDIT = "कोई image नहीं मिली। पहले image बनाओ या upload करो, फिर edit करूंगी।"
ERROR_IMAGE_GENERATE_FAILED = "Image generate नहीं हो सकी।"
ERROR_SERVER_BUSY = "Server busy है। थोड़ी देर में try करो।"
```

**File: `/home/ubuntu/pipecat/src/bot/core/helpers.py` (Lines 11-56)**

Language-based error selection:
```python
def _is_hindi(language: str = None) -> bool:
    """Check if the session language is Hindi (default for India)."""
    return language in (None, "hi")

def get_error_messages(error, generation_type: str = "image", language: str = None):
    hi = _is_hindi(language)
    if isinstance(error, ModerationError):
        msg = constants.ERROR_IMAGE_EDIT_FAILED if hi else constants.ERROR_IMAGE_EDIT_FAILED_EN
```

### 7. **Device Language Storage & Region Detection**

**File: `/home/ubuntu/pipecat/src/repository/device_repository.py` (Lines 19-23)**

Region determination affects language selection:
```python
def determine_region(tz: str) -> str:
    """Map timezone to region. Asia/Kolkata = india (Hindi region)"""
    if tz in ("Asia/Kolkata", "Asia/Calcutta"):
        return "india"
    return "global"
```

**File: `/home/ubuntu/pipecat/src/service/app_v2_service.py` (Lines 12-22, 25-102)**

Language is set at onboarding and stored in device table:
```python
async def set_device_language(*, device_id: str, language: str) -> dict:
    """Called after user picks language (hi for Hindi)"""
    success = await device_repo.onboard_device(device_id, region, language, country)
    logger.info(f"DEVICE_ONBOARDED: device_id={device_id} region={region} language={language}")
```

### 8. **API Entry Points**

**File: `/home/ubuntu/pipecat/src/routes/bot.py` (Lines 178-219)**

Language is passed in session connection:
```python
@router.post("/start")
async def bot_connect(data: BotConnectRequest) -> Dict[Any, Any]:
    language = data.language

    params = {
        "language": language,  # Passed to pipeline
    }
```

**File: `/home/ubuntu/pipecat/src/routes/v2.py` (Lines 25-53)**

On app open, Hindi is returned as option for India region:
```python
@router.post("/set_language")
async def set_language(device_id: str, language: str):
    """Called after user selects 'hi' on onboarding screen"""
    result = await set_device_language(device_id=device_id, language=language)

@router.get("/on_open")
async def on_open(device_id: str = None, timezone: str = None):
    """Returns languages filtered by region"""
    languages = await config_v2_repo.get_languages(region=region)
    # India region → includes Hindi
```

---

## Key Findings

1. **Hindi is the DEFAULT language** (`language or "hi"` in load_system_instruction)
2. **DB-stored language takes precedence** over frontend-sent language
3. **Language determines:**
   - STT language (Deepgram nova-3 for mobile, Gemini Live for web)
   - TTS language (Veena3 TTS speaker, customizable per persona)
   - System prompt instructions (language-specific and persona-specific intros)
   - Error messages (bilingual Hindi/English)
4. **Region-based routing:** Asia/Kolkata timezone → India region → Hindi + Telugu options
5. **Gemini Live uses `Language.HI_IN` enum** for Hindi with full STT+TTS support
6. **Config stored in `configurations_v2` DB table** with caching (300s TTL)