The file /home/ubuntu/pipecat/src/bot/services/video_service.py has been updated successfully.

        block_reason = _can_gen(bot_session)
        if block_reason:
            await send_limit_popup(bot_session, rtvi_processor, block_reason)
            if block_reason == "cooldown":
                cooldown = bot_session.cooldown_minutes
                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_video again."
            else:
                instruction = "User has reached their generation limit. Tell them politely. Do NOT call generate_video again."
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": instruction,
            }))
            return

        hi = _is_hindi(bot_session.language)

        if not prompt:
            no_prompt_msg = "Kaisa video chahiye aapko? Please batao kya banau." if hi else "What kind of video would you like? Please describe what to create."

"""Video generation service using Replicate API (text-to-video and image-to-video)."""

import os
import uuid
import time
import json
import asyncio
from loguru import logger

from pipecat.processors.frameworks.rtvi import RTVIProcessor
from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.services.llm_service import FunctionCallParams
from pipecat.frames.frames import TTSSpeakFrame

from src.bot.core.session_state import SessionState
from src.config import constants
from src.core.storage import download_and_upload_to_s3

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from src.bot.core.config import BotSessionState, TranscriptManager

# ── Function schemas ────────────────────────────────────────────

generate_video_function = FunctionSchema(
    name="generate_video",
    description=(
        "Generate a video from a text description using AI. Use this function when the user asks to "
        "create, make, or generate a VIDEO (not image) WITHOUT having uploaded an image. "
        "Examples: 'make a video of a car driving', 'create a video of sunset', 'generate video of dancing' "
        "CRITICAL: This is for VIDEO generation only, NOT images. "
        "Do NOT call this function if video generation is already in progress. "
        "If user has uploaded an image and wants to make a video FROM that image, use generate_video_from_image instead."
    ),
    properties={
        "prompt": {
            "type": "string",
            "description": "Detailed description of the video to generate (e.g., 'A sports car driving fast along a beach at sunset')"
        }
    },
    required=["prompt"]
)

generate_video_from_image_function = FunctionSchema(
    name="generate_video_from_image",
    description=(
        "Generate a video from an UPLOADED IMAGE using AI. Use this function when: "
        "1. User has ALREADY uploaded an image, AND "
        "2. User wants to create/make a VIDEO from that uploaded image. "
        "Examples: 'make video from this image', 'animate this picture', 'turn this into a video', "
        "'video banao is image se', 'is photo ko animate karo' "
        "CRITICAL: Only use when user has uploaded an image AND wants video from it. "
        "Do NOT use for text-to-video (use generate_video instead). "
        "Do NOT call if video generation is already in progress."
    ),
    properties={
        "prompt": {
            "type": "string",
            "description": "Description of how to animate/move the image (e.g., 'The person slowly smiles and waves', 'The car starts moving forward')"
        }
    },
    required=["prompt"]
)

# ── Shared helpers ──────────────────────────────────────────────

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

async def _tts_speak(llm_service, text: str, session_info: str, label: str):
    """Push a TTS frame, logging failures."""
    if not llm_service:
        return
    try:
        await llm_service.push_frame(TTSSpeakFrame(text))
    except Exception as e:
        logger.error(f"{label}_TTS_ERROR: {session_info} error={e}")

async def _reject(params, llm_service, session_info, label, status, reason, tts_text, do_not_speak):
    """Speak TTS message and send reject callback in one call."""
    await _tts_speak(llm_service, tts_text, session_info, label)
    await params.result_callback(json.dumps({
        "status": status, "reason": reason, "user_informed": True, "DO_NOT_SPEAK": do_not_speak,
    }))

async def _start_video_generation(bot_session, rtvi_processor, llm_service, session_info,
                                   request_id, params, label, tts_text, started_data,
                                   model, replicate_input, video_type, prompt, extra_complete_data=None,
                                   language: str = None):
    """Common flow: set lock → send RTVI started → TTS → callback → background task."""
    bot_session.is_processing = True

    await rtvi_processor.send_server_message({"type": "video_generation_started", "data": started_data})
    await _tts_speak(llm_service, tts_text, session_info, label)
    await params.result_callback(json.dumps({
        "status": "in_progress", "request_id": request_id, "user_informed": True,
        "DO_NOT_SPEAK": f"Video generation started. User informed via voice. Do NOT call {video_type} again or speak about it."
    }))

    asyncio.create_task(_run_video_background(
        bot_session, rtvi_processor, llm_service,
        session_info, request_id, bot_session.device_id,
        model=model, replicate_input=replicate_input,
        video_type=video_type, label=label, prompt=prompt,
        extra_complete_data=extra_complete_data,
        language=language,
    ))

async def _run_video_background(
    bot_session, rtvi_processor, llm_service,
    session_info, request_id, device_id,
    model: str, replicate_input: dict,
    video_type: str, label: str,
    prompt: str, extra_complete_data: dict = None,
    language: str = None,
):
    """Background task for video generation (t2v and i2v)."""
    session_id = bot_session.session_id
    try:
        import replicate
        os.environ["REPLICATE_API_TOKEN"] = constants.REPLICATE_API_TOKEN

        start_time = time.time()
        logger.info(f"{label}_REPLICATE_START: {session_info} request_id={request_id}")

        output = await asyncio.to_thread(lambda: replicate.run(model, input=replicate_input))
        replicate_url = output.url if hasattr(output, 'url') else str(output)

        replicate_time = time.time() - start_time
        logger.info(f"{label}_REPLICATE_COMPLETE: {session_info} request_id={request_id} replicate_time={replicate_time:.2f}s")

        video_id = f"{video_type[:3]}_{request_id}"
        s3_key = f"{device_id}/{session_id}/videos/{video_id}.mp4"

        try:
            s3_url = await download_and_upload_to_s3(replicate_url, s3_key, "video/mp4")
            logger.info(f"{label}_S3_UPLOADED: {session_info} request_id={request_id}")
        except Exception as s3_err:
            logger.error(f"{label}_S3_UPLOAD_FAILED: {session_info} request_id={request_id} error={s3_err}")
            s3_url = replicate_url

        total_time = time.time() - start_time

        bot_session.add_generated_video(
            video_type=video_type,
            metadata={"video_id": video_id, "s3_key": s3_key, "replicate_url": replicate_url,
                       "model": model, "replicate_time": replicate_time, "total_time": total_time},
            prompt=prompt,
        )

        complete_data = {"request_id": request_id, "video_url": s3_url, "video_id": video_id,
                         "prompt": prompt, "duration": total_time, "video_model": model}
        if extra_complete_data:
            complete_data.update(extra_complete_data)

        await rtvi_processor.send_server_message({"type": "video_generation_complete", "data": complete_data})
        logger.info(f"{label}_COMPLETE: {session_info} request_id={request_id}")

        # Decrement credit
        from src.bot.services.limit_checker import use_credit
        use_credit(bot_session)

        ready_msg = "Tumhara video ready hai! Screen pe dekho!" if _is_hindi(language) else "Your video is ready! Check it out on screen!"
        await _tts_speak(llm_service, ready_msg, session_info, label)

    except asyncio.CancelledError:
        logger.warning(f"{label}_CANCELLED: {session_info} request_id={request_id}")
    except Exception as e:
        logger.error(f"{label}_ERROR: {session_info} request_id={request_id} error={str(e)}")
        try:
            await rtvi_processor.send_server_message({
                "type": "video_generation_error", "data": {"request_id": request_id, "error": str(e)}
            })
        except Exception:
            pass
    finally:
        bot_session.is_processing = False

# ── Handler factories ───────────────────────────────────────────

def create_video_handler(
    state: SessionState,
    rtvi_processor: RTVIProcessor,
    bot_session: "BotSessionState",
    transcript_manager: "TranscriptManager",
    task=None,
    llm_service=None,
):
    """Create text-to-video handler."""

    async def handle_generate_video(params: FunctionCallParams):
        request_id = str(uuid.uuid4())[:8]
        session_info = bot_session.get_session_info()
        prompt = params.arguments.get("prompt", "")
        label = "VIDEO"

        logger.info(f"{label}_REQUEST: {session_info} request_id={request_id} prompt={prompt[:50]}...")

        # Check limits
        from src.bot.services.limit_checker import can_generate as _can_gen, send_limit_popup
        block_reason = _can_gen(bot_session)
        if block_reason:
            await send_limit_popup(bot_session, rtvi_processor, block_reason)
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": "User has reached their generation limit. Tell them politely. Do NOT call generate_video again.",
            }))
            return

        hi = _is_hindi(bot_session.language)

        if not prompt:
            no_prompt_msg = "Kaisa video chahiye aapko? Please batao kya banau." if hi else "What kind of video would you like? Please describe what to create."
            await _reject(params, llm_service, session_info, label, "need_more_info", "no_prompt",
                no_prompt_msg,
                "User was asked for video description via voice. Wait for their response.")
            return

        if bot_session.is_processing:
            busy_msg = "Abhi ek video ya image already process ho rahi hai. Please thoda wait karo." if hi else "A video or image is already being processed. Please wait a moment."
            await _reject(params, llm_service, session_info, label, "busy", "already_processing",
                busy_msg,
                "User told to wait via voice. Do NOT repeat or acknowledge.")
            return

        start_msg = "Ruko, tumhara video ban raha hai! Thoda wait karo, 1-2 minute mein ready ho jayega." if hi else "Your video is being generated! Please wait, it'll be ready in 1-2 minutes."
        await _start_video_generation(
            bot_session, rtvi_processor, llm_service, session_info, request_id, params, label,
            tts_text=start_msg,
            started_data={"request_id": request_id, "prompt": prompt, "video_model": constants.REPLICATE_VIDEO_MODEL},
            model=constants.REPLICATE_VIDEO_MODEL,
            replicate_input={"prompt": prompt, "resolution": "480p"},
            video_type="text_to_video", prompt=prompt,
            language=bot_session.language,
        )

    return handle_generate_video

def create_video_from_image_handler(
    state: SessionState,
    rtvi_processor: RTVIProcessor,
    bot_session: "BotSessionState",
    transcript_manager: "TranscriptManager",
    task=None,
    llm_service=None,
):
    """Create image-to-video handler."""

    async def handle_generate_video_from_image(params: FunctionCallParams):
        request_id = str(uuid.uuid4())[:8]
        session_info = bot_session.get_session_info()
        prompt = params.arguments.get("prompt", "")
        label = "I2V"

        logger.info(f"{label}_REQUEST: {session_info} request_id={request_id} prompt={prompt[:50]}...")

        # Check limits
        from src.bot.services.limit_checker import can_generate as _can_gen, send_limit_popup
        block_reason = _can_gen(bot_session)
        if block_reason:
            await send_limit_popup(bot_session, rtvi_processor, block_reason)
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": "User has reached their generation limit. Tell them politely. Do NOT call generate_video_from_image again.",
            }))
            return

        hi = _is_hindi(bot_session.language)

        # Validate uploaded image
        uploaded_image = bot_session.get_uploaded_image()
        if not uploaded_image:
            no_img_msg = "Pehle ek image upload karo, phir video bana sakte ho!" if hi else "Please upload an image first, then we can make a video from it!"
            await _reject(params, llm_service, session_info, label, "error", "no_image_uploaded",
                no_img_msg,
                "User was asked to upload image via voice. Wait for their response.")
            return

        image_url = uploaded_image.get("s3_url")
        if not image_url:
            reupload_msg = "Image upload mein kuch problem hai. Please dobara upload karo." if hi else "There was a problem with the image upload. Please upload again."
            await _reject(params, llm_service, session_info, label, "error", "image_url_missing",
                reupload_msg,
                "User told to re-upload. Wait for their response.")
            return

        if not prompt:
            image_desc = uploaded_image.get("description", "")
            if image_desc:
                prompt = f"Animate this: {image_desc}"
            else:
                animate_msg = "Is image ko kaise animate karna hai? Batao kya motion chahiye." if hi else "How would you like to animate this image? Describe the motion you want."
                await _reject(params, llm_service, session_info, label, "need_more_info", "no_prompt",
                    animate_msg,
                    "User was asked for animation description via voice. Wait for their response.")
                return

        if bot_session.is_processing:
            busy_msg = "Abhi ek video ya image already process ho rahi hai. Please thoda wait karo." if hi else "A video or image is already being processed. Please wait a moment."
            await _reject(params, llm_service, session_info, label, "busy", "already_processing",
                busy_msg,
                "User told to wait via voice. Do NOT repeat or acknowledge.")
            return

        start_msg = "Ruko, tumhari image se video ban raha hai! Thoda wait karo, 1-2 minute mein ready ho jayega." if hi else "Your image is being turned into a video! Please wait, it'll be ready in 1-2 minutes."
        await _start_video_generation(
            bot_session, rtvi_processor, llm_service, session_info, request_id, params, label,
            tts_text=start_msg,
            started_data={"request_id": request_id, "prompt": prompt, "source_image": image_url,
                          "video_model": constants.REPLICATE_I2V_MODEL, "type": "i2v"},
            model=constants.REPLICATE_I2V_MODEL,
            replicate_input={
                "image": image_url, "prompt": prompt, "go_fast": True,
                "num_frames": 81, "resolution": "480p", "sample_shift": 12,
                "frames_per_second": 16, "interpolate_output": False,
                "lora_scale_transformer": 1, "lora_scale_transformer_2": 1,
            },
            video_type="image_to_video", prompt=prompt,
            extra_complete_data={"source_image": image_url, "type": "i2v"},
            language=bot_session.language,
        )

    return handle_generate_video_from_image

+            if block_reason == "cooldown":

+                cooldown = bot_session.cooldown_minutes

+                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_video again."

+                instruction = "User has reached their generation limit. Tell them politely. Do NOT call generate_video again."

-                "assistant_instruction": "User has reached their generation limit. Tell them politely. Do NOT call generate_video again.",

+                "assistant_instruction": instruction,


Now the second video handler (generate_video_from_image):


264	        request_id = str(uuid.uuid4())[:8]
265	        session_info = bot_session.get_session_info()
266	        prompt = params.arguments.get("prompt", "")
267	        label = "I2V"
268
269	        logger.info(f"{label}_REQUEST: {session_info} request_id={request_id} prompt={prompt[:50]}...")
270
271	        # Check limits
272	        from src.bot.services.limit_checker import can_generate as _can_gen, send_limit_popup
273	        block_reason = _can_gen(bot_session)
274	        if block_reason:
275	            await send_limit_popup(bot_session, rtvi_processor, block_reason)
276	            await params.result_callback(json.dumps({
277	                "status": "limit_reached",
278	                "assistant_instruction": "User has reached their generation limit. Tell them politely. Do NOT call generate_video_from_image again.",

        request_id = str(uuid.uuid4())[:8]
        session_info = bot_session.get_session_info()
        prompt = params.arguments.get("prompt", "")
        label = "I2V"

        logger.info(f"{label}_REQUEST: {session_info} request_id={request_id} prompt={prompt[:50]}...")

        # Check limits
        from src.bot.services.limit_checker import can_generate as _can_gen, send_limit_popup
        block_reason = _can_gen(bot_session)
        if block_reason:
            await send_limit_popup(bot_session, rtvi_processor, block_reason)
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": "User has reached their generation limit. Tell them politely. Do NOT call generate_video_from_image again.",


Edit

        # Check limits
        from src.bot.services.limit_checker import can_generate as _can_gen, send_limit_popup
        block_reason = _can_gen(bot_session)
        if block_reason:
            await send_limit_popup(bot_session, rtvi_processor, block_reason)
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": "User has reached their generation limit. Tell them politely. Do NOT call generate_video_from_image again.",

        # Check limits
        from src.bot.services.limit_checker import can_generate as _can_gen, send_limit_popup
        block_reason = _can_gen(bot_session)
        if block_reason:
            await send_limit_popup(bot_session, rtvi_processor, block_reason)
            if block_reason == "cooldown":
                cooldown = bot_session.cooldown_minutes
                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_video_from_image again."
            else:
                instruction = "User has reached their generation limit. Tell them politely. Do NOT call generate_video_from_image again."
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": instruction,


        # Check limits
        from src.bot.services.limit_checker import can_generate as _can_gen, send_limit_popup
        block_reason = _can_gen(bot_session)
        if block_reason:
            await send_limit_popup(bot_session, rtvi_processor, block_reason)
            if block_reason == "cooldown":
                cooldown = bot_session.cooldown_minutes
                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_video_from_image again."
            else:
                instruction = "User has reached their generation limit. Tell them politely. Do NOT call generate_video_from_image again."
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": instruction,

"""Video generation service using Replicate API (text-to-video and image-to-video)."""

import os
import uuid
import time
import json
import asyncio
from loguru import logger

from pipecat.processors.frameworks.rtvi import RTVIProcessor
from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.services.llm_service import FunctionCallParams
from pipecat.frames.frames import TTSSpeakFrame

from src.bot.core.session_state import SessionState
from src.config import constants
from src.core.storage import download_and_upload_to_s3

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from src.bot.core.config import BotSessionState, TranscriptManager

# ── Function schemas ────────────────────────────────────────────

generate_video_function = FunctionSchema(
    name="generate_video",
    description=(
        "Generate a video from a text description using AI. Use this function when the user asks to "
        "create, make, or generate a VIDEO (not image) WITHOUT having uploaded an image. "
        "Examples: 'make a video of a car driving', 'create a video of sunset', 'generate video of dancing' "
        "CRITICAL: This is for VIDEO generation only, NOT images. "
        "Do NOT call this function if video generation is already in progress. "
        "If user has uploaded an image and wants to make a video FROM that image, use generate_video_from_image instead."
    ),
    properties={
        "prompt": {
            "type": "string",
            "description": "Detailed description of the video to generate (e.g., 'A sports car driving fast along a beach at sunset')"
        }
    },
    required=["prompt"]
)

generate_video_from_image_function = FunctionSchema(
    name="generate_video_from_image",
    description=(
        "Generate a video from an UPLOADED IMAGE using AI. Use this function when: "
        "1. User has ALREADY uploaded an image, AND "
        "2. User wants to create/make a VIDEO from that uploaded image. "
        "Examples: 'make video from this image', 'animate this picture', 'turn this into a video', "
        "'video banao is image se', 'is photo ko animate karo' "
        "CRITICAL: Only use when user has uploaded an image AND wants video from it. "
        "Do NOT use for text-to-video (use generate_video instead). "
        "Do NOT call if video generation is already in progress."
    ),
    properties={
        "prompt": {
            "type": "string",
            "description": "Description of how to animate/move the image (e.g., 'The person slowly smiles and waves', 'The car starts moving forward')"
        }
    },
    required=["prompt"]
)

# ── Shared helpers ──────────────────────────────────────────────

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

async def _tts_speak(llm_service, text: str, session_info: str, label: str):
    """Push a TTS frame, logging failures."""
    if not llm_service:
        return
    try:
        await llm_service.push_frame(TTSSpeakFrame(text))
    except Exception as e:
        logger.error(f"{label}_TTS_ERROR: {session_info} error={e}")

async def _reject(params, llm_service, session_info, label, status, reason, tts_text, do_not_speak):
    """Speak TTS message and send reject callback in one call."""
    await _tts_speak(llm_service, tts_text, session_info, label)
    await params.result_callback(json.dumps({
        "status": status, "reason": reason, "user_informed": True, "DO_NOT_SPEAK": do_not_speak,
    }))

async def _start_video_generation(bot_session, rtvi_processor, llm_service, session_info,
                                   request_id, params, label, tts_text, started_data,
                                   model, replicate_input, video_type, prompt, extra_complete_data=None,
                                   language: str = None):
    """Common flow: set lock → send RTVI started → TTS → callback → background task."""
    bot_session.is_processing = True

    await rtvi_processor.send_server_message({"type": "video_generation_started", "data": started_data})
    await _tts_speak(llm_service, tts_text, session_info, label)
    await params.result_callback(json.dumps({
        "status": "in_progress", "request_id": request_id, "user_informed": True,
        "DO_NOT_SPEAK": f"Video generation started. User informed via voice. Do NOT call {video_type} again or speak about it."
    }))

    asyncio.create_task(_run_video_background(
        bot_session, rtvi_processor, llm_service,
        session_info, request_id, bot_session.device_id,
        model=model, replicate_input=replicate_input,
        video_type=video_type, label=label, prompt=prompt,
        extra_complete_data=extra_complete_data,
        language=language,
    ))

async def _run_video_background(
    bot_session, rtvi_processor, llm_service,
    session_info, request_id, device_id,
    model: str, replicate_input: dict,
    video_type: str, label: str,
    prompt: str, extra_complete_data: dict = None,
    language: str = None,
):
    """Background task for video generation (t2v and i2v)."""
    session_id = bot_session.session_id
    try:
        import replicate
        os.environ["REPLICATE_API_TOKEN"] = constants.REPLICATE_API_TOKEN

        start_time = time.time()
        logger.info(f"{label}_REPLICATE_START: {session_info} request_id={request_id}")

        output = await asyncio.to_thread(lambda: replicate.run(model, input=replicate_input))
        replicate_url = output.url if hasattr(output, 'url') else str(output)

        replicate_time = time.time() - start_time
        logger.info(f"{label}_REPLICATE_COMPLETE: {session_info} request_id={request_id} replicate_time={replicate_time:.2f}s")

        video_id = f"{video_type[:3]}_{request_id}"
        s3_key = f"{device_id}/{session_id}/videos/{video_id}.mp4"

        try:
            s3_url = await download_and_upload_to_s3(replicate_url, s3_key, "video/mp4")
            logger.info(f"{label}_S3_UPLOADED: {session_info} request_id={request_id}")
        except Exception as s3_err:
            logger.error(f"{label}_S3_UPLOAD_FAILED: {session_info} request_id={request_id} error={s3_err}")
            s3_url = replicate_url

        total_time = time.time() - start_time

        bot_session.add_generated_video(
            video_type=video_type,
            metadata={"video_id": video_id, "s3_key": s3_key, "replicate_url": replicate_url,
                       "model": model, "replicate_time": replicate_time, "total_time": total_time},
            prompt=prompt,
        )

        complete_data = {"request_id": request_id, "video_url": s3_url, "video_id": video_id,
                         "prompt": prompt, "duration": total_time, "video_model": model}
        if extra_complete_data:
            complete_data.update(extra_complete_data)

        await rtvi_processor.send_server_message({"type": "video_generation_complete", "data": complete_data})
        logger.info(f"{label}_COMPLETE: {session_info} request_id={request_id}")

        # Decrement credit
        from src.bot.services.limit_checker import use_credit
        use_credit(bot_session)

        ready_msg = "Tumhara video ready hai! Screen pe dekho!" if _is_hindi(language) else "Your video is ready! Check it out on screen!"
        await _tts_speak(llm_service, ready_msg, session_info, label)

    except asyncio.CancelledError:
        logger.warning(f"{label}_CANCELLED: {session_info} request_id={request_id}")
    except Exception as e:
        logger.error(f"{label}_ERROR: {session_info} request_id={request_id} error={str(e)}")
        try:
            await rtvi_processor.send_server_message({
                "type": "video_generation_error", "data": {"request_id": request_id, "error": str(e)}
            })
        except Exception:
            pass
    finally:
        bot_session.is_processing = False

# ── Handler factories ───────────────────────────────────────────

def create_video_handler(
    state: SessionState,
    rtvi_processor: RTVIProcessor,
    bot_session: "BotSessionState",
    transcript_manager: "TranscriptManager",
    task=None,
    llm_service=None,
):
    """Create text-to-video handler."""

    async def handle_generate_video(params: FunctionCallParams):
        request_id = str(uuid.uuid4())[:8]
        session_info = bot_session.get_session_info()
        prompt = params.arguments.get("prompt", "")
        label = "VIDEO"

        logger.info(f"{label}_REQUEST: {session_info} request_id={request_id} prompt={prompt[:50]}...")

        # Check limits
        from src.bot.services.limit_checker import can_generate as _can_gen, send_limit_popup
        block_reason = _can_gen(bot_session)
        if block_reason:
            await send_limit_popup(bot_session, rtvi_processor, block_reason)
            if block_reason == "cooldown":
                cooldown = bot_session.cooldown_minutes
                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_video again."
            else:
                instruction = "User has reached their generation limit. Tell them politely. Do NOT call generate_video again."
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": instruction,
            }))
            return

        hi = _is_hindi(bot_session.language)

        if not prompt:
            no_prompt_msg = "Kaisa video chahiye aapko? Please batao kya banau." if hi else "What kind of video would you like? Please describe what to create."
            await _reject(params, llm_service, session_info, label, "need_more_info", "no_prompt",
                no_prompt_msg,
                "User was asked for video description via voice. Wait for their response.")
            return

        if bot_session.is_processing:
            busy_msg = "Abhi ek video ya image already process ho rahi hai. Please thoda wait karo." if hi else "A video or image is already being processed. Please wait a moment."
            await _reject(params, llm_service, session_info, label, "busy", "already_processing",
                busy_msg,
                "User told to wait via voice. Do NOT repeat or acknowledge.")
            return

        start_msg = "Ruko, tumhara video ban raha hai! Thoda wait karo, 1-2 minute mein ready ho jayega." if hi else "Your video is being generated! Please wait, it'll be ready in 1-2 minutes."
        await _start_video_generation(
            bot_session, rtvi_processor, llm_service, session_info, request_id, params, label,
            tts_text=start_msg,
            started_data={"request_id": request_id, "prompt": prompt, "video_model": constants.REPLICATE_VIDEO_MODEL},
            model=constants.REPLICATE_VIDEO_MODEL,
            replicate_input={"prompt": prompt, "resolution": "480p"},
            video_type="text_to_video", prompt=prompt,
            language=bot_session.language,
        )

    return handle_generate_video

def create_video_from_image_handler(
    state: SessionState,
    rtvi_processor: RTVIProcessor,
    bot_session: "BotSessionState",
    transcript_manager: "TranscriptManager",
    task=None,
    llm_service=None,
):
    """Create image-to-video handler."""

    async def handle_generate_video_from_image(params: FunctionCallParams):
        request_id = str(uuid.uuid4())[:8]
        session_info = bot_session.get_session_info()
        prompt = params.arguments.get("prompt", "")
        label = "I2V"

        logger.info(f"{label}_REQUEST: {session_info} request_id={request_id} prompt={prompt[:50]}...")

        # Check limits
        from src.bot.services.limit_checker import can_generate as _can_gen, send_limit_popup
        block_reason = _can_gen(bot_session)
        if block_reason:
            await send_limit_popup(bot_session, rtvi_processor, block_reason)
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": "User has reached their generation limit. Tell them politely. Do NOT call generate_video_from_image again.",
            }))
            return

        hi = _is_hindi(bot_session.language)

        # Validate uploaded image
        uploaded_image = bot_session.get_uploaded_image()
        if not uploaded_image:
            no_img_msg = "Pehle ek image upload karo, phir video bana sakte ho!" if hi else "Please upload an image first, then we can make a video from it!"
            await _reject(params, llm_service, session_info, label, "error", "no_image_uploaded",
                no_img_msg,
                "User was asked to upload image via voice. Wait for their response.")
            return

        image_url = uploaded_image.get("s3_url")
        if not image_url:
            reupload_msg = "Image upload mein kuch problem hai. Please dobara upload karo." if hi else "There was a problem with the image upload. Please upload again."
            await _reject(params, llm_service, session_info, label, "error", "image_url_missing",
                reupload_msg,
                "User told to re-upload. Wait for their response.")
            return

        if not prompt:
            image_desc = uploaded_image.get("description", "")
            if image_desc:
                prompt = f"Animate this: {image_desc}"
            else:
                animate_msg = "Is image ko kaise animate karna hai? Batao kya motion chahiye." if hi else "How would you like to animate this image? Describe the motion you want."
                await _reject(params, llm_service, session_info, label, "need_more_info", "no_prompt",
                    animate_msg,
                    "User was asked for animation description via voice. Wait for their response.")
                return

        if bot_session.is_processing:
            busy_msg = "Abhi ek video ya image already process ho rahi hai. Please thoda wait karo." if hi else "A video or image is already being processed. Please wait a moment."
            await _reject(params, llm_service, session_info, label, "busy", "already_processing",
                busy_msg,
                "User told to wait via voice. Do NOT repeat or acknowledge.")
            return

        start_msg = "Ruko, tumhari image se video ban raha hai! Thoda wait karo, 1-2 minute mein ready ho jayega." if hi else "Your image is being turned into a video! Please wait, it'll be ready in 1-2 minutes."
        await _start_video_generation(
            bot_session, rtvi_processor, llm_service, session_info, request_id, params, label,
            tts_text=start_msg,
            started_data={"request_id": request_id, "prompt": prompt, "source_image": image_url,
                          "video_model": constants.REPLICATE_I2V_MODEL, "type": "i2v"},
            model=constants.REPLICATE_I2V_MODEL,
            replicate_input={
                "image": image_url, "prompt": prompt, "go_fast": True,
                "num_frames": 81, "resolution": "480p", "sample_shift": 12,
                "frames_per_second": 16, "interpolate_output": False,
                "lora_scale_transformer": 1, "lora_scale_transformer_2": 1,
            },
            video_type="image_to_video", prompt=prompt,
            extra_complete_data={"source_image": image_url, "type": "i2v"},
            language=bot_session.language,
        )

    return handle_generate_video_from_image

+                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_video_from_image again."

+                instruction = "User has reached their generation limit. Tell them politely. Do NOT call generate_video_from_image again."

-                "assistant_instruction": "User has reached their generation limit. Tell them politely. Do NOT call generate_video_from_image again.",


Now music and ppt:


162	            }))
163	            return
164
165	        # Credit check (3-layer: plan → topup → free)
166	        from src.bot.services.limit_checker import can_generate as check_limits, send_limit_popup
167	        limit_reason = check_limits(bot_session)
168	        if limit_reason:
169	            await send_limit_popup(bot_session, rtvi, limit_reason)
170	            await params.result_callback(json.dumps({
171	                "status": "limit_reached",
172	                "assistant_instruction": "The user has used all their music credits. Tell them to subscribe or buy a top-up pack.",
173	            }))
174	            return
175
176	        bot_session.is_processing = True
177	        music_id = f"music_{int(time.time())}_{uuid.uuid4().hex[:4]}"
178	        await _send_rtvi(task, {"type": "generation_started", "data": {"type": "music", "status": "in_progress"}})
179
180	        try:
181	            logger.info(f"MUSIC_GENERATE_START: session={session_id} prompt={prompt[:80]}")

            }))
            return

        # Credit check (3-layer: plan → topup → free)
        from src.bot.services.limit_checker import can_generate as check_limits, send_limit_popup
        limit_reason = check_limits(bot_session)
        if limit_reason:
            await send_limit_popup(bot_session, rtvi, limit_reason)
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": "The user has used all their music credits. Tell them to subscribe or buy a top-up pack.",
            }))
            return

        bot_session.is_processing = True
        music_id = f"music_{int(time.time())}_{uuid.uuid4().hex[:4]}"
        await _send_rtvi(task, {"type": "generation_started", "data": {"type": "music", "status": "in_progress"}})

        try:
            logger.info(f"MUSIC_GENERATE_START: session={session_id} prompt={prompt[:80]}")


        limit_reason = check_limits(bot_session)
        if limit_reason:
            await send_limit_popup(bot_session, rtvi, limit_reason)
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": "The user has used all their music credits. Tell them to subscribe or buy a top-up pack.",
            }))

        limit_reason = check_limits(bot_session)
        if limit_reason:
            await send_limit_popup(bot_session, rtvi, limit_reason)
            if limit_reason == "cooldown":
                cooldown = bot_session.cooldown_minutes
                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_music again."
            else:
                instruction = "The user has used all their music credits. Tell them to subscribe or buy a top-up pack."
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": instruction,
            }))


The file /home/ubuntu/pipecat/src/bot/services/music_service.py has been updated successfully.

        limit_reason = check_limits(bot_session)
        if limit_reason:
            await send_limit_popup(bot_session, rtvi, limit_reason)
            if limit_reason == "cooldown":
                cooldown = bot_session.cooldown_minutes
                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_music again."
            else:
                instruction = "The user has used all their music credits. Tell them to subscribe or buy a top-up pack."
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": instruction,
            }))

"""Music generation service using Google Lyria 2 with GCS storage."""

import asyncio
import base64
import json
import os
import time
import uuid

import httpx
from loguru import logger
from google.oauth2 import service_account
from google.auth.transport.requests import Request as AuthRequest

from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.frames.frames import OutputTransportMessageUrgentFrame
from pipecat.services.llm_service import FunctionCallParams

from src.core.storage import upload_to_s3
from src.config import constants

# --- Function Schema ---

generate_music_function = FunctionSchema(
    name="generate_music",
    description=(
        "Generate a music track from a text description using AI. "
        "The user describes the style, mood, instruments, or genre and you create music. "
        "CRITICAL: Do NOT call this function again if generation is already in progress."
    ),
    properties={
        "prompt": {
            "type": "string",
            "description": "Text description of the music to generate (e.g. 'upbeat Bollywood dance track with tabla and sitar')",
        },
    },
    required=["prompt"],
)

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from src.bot.core.config import BotSessionState, TranscriptManager

LYRIA_LOCATION = "us-central1"
LYRIA_MODEL = "lyria-002"
AUDIO_DURATION_SECS = 32.8  # Lyria 2 generates ~32.8s clips

# --- Helpers ---

async def _send_rtvi(task, data: dict):
    try:
        frame = OutputTransportMessageUrgentFrame(message={
            "label": "rtvi-ai", "type": "server-message", "data": data,
        })
        await task.queue_frame(frame)
    except Exception as e:
        logger.warning(f"Failed to send RTVI message: {e}")

def _get_access_token() -> str:
    """Get GCP access token using service account credentials."""
    creds = service_account.Credentials.from_service_account_file(
        constants.GCP_CREDENTIALS_PATH,
        scopes=["https://www.googleapis.com/auth/cloud-platform"],
    )
    creds.refresh(AuthRequest())
    return creds.token

async def _generate_music(prompt: str) -> bytes:
    """Call Lyria 2 predict endpoint and return WAV audio bytes."""
    project_id = constants.GOOGLE_CLOUD_PROJECT
    token = await asyncio.get_event_loop().run_in_executor(None, _get_access_token)

    url = (
        f"https://{LYRIA_LOCATION}-aiplatform.googleapis.com/v1/"
        f"projects/{project_id}/locations/{LYRIA_LOCATION}/"
        f"publishers/google/models/{LYRIA_MODEL}:predict"
    )

    payload = {
        "instances": [
            {"prompt": prompt}
        ],
        "parameters": {}
    }

    async with httpx.AsyncClient(timeout=120.0) as client:
        response = await client.post(
            url,
            headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
            },
            json=payload,
        )
        response.raise_for_status()
        data = response.json()

    # Log response structure for debugging
    top_keys = list(data.keys())
    logger.info(f"LYRIA_RESPONSE_KEYS: {top_keys}")

    predictions = data.get("predictions", [])
    if not predictions:
        raise Exception(f"No predictions in Lyria response: {top_keys}")

    pred = predictions[0]
    pred_keys = list(pred.keys()) if isinstance(pred, dict) else [type(pred).__name__]
    logger.info(f"LYRIA_PREDICTION_KEYS: {pred_keys}")

    audio_b64 = pred.get("bytesBase64Encoded", "") or pred.get("audioContent", "")
    if not audio_b64:
        logger.error(f"LYRIA_NO_AUDIO: prediction_keys={pred_keys}")
        raise Exception(f"No audio data in Lyria prediction, keys={pred_keys}")

    audio_bytes = base64.b64decode(audio_b64)
    logger.info(f"LYRIA_AUDIO_DECODED: size={len(audio_bytes)} bytes")
    return audio_bytes

async def _store_audio(audio_bytes: bytes, session_id: str, device_id: str) -> str:
    """Upload WAV audio to GCS and return public URL."""
    audio_key = f"{device_id}/{session_id}/music/{uuid.uuid4().hex[:12]}.wav"
    audio_url = await upload_to_s3(audio_bytes, audio_key, "audio/wav")
    logger.info(f"MUSIC_UPLOADED: session={session_id} key={audio_key}")
    return audio_url

# --- Handler factory ---

def create_music_handler(
    session_state,
    rtvi,
    bot_session: "BotSessionState",
    transcript_manager: "TranscriptManager",
    task,
    llm,
):
    """Create music generation handler."""

    async def handle_generate_music(params: FunctionCallParams):
        args = params.arguments
        prompt = args.get("prompt", "")
        session_id = bot_session.session_id or "unknown"
        device_id = bot_session.device_id or "common"

        if not prompt:
            await params.result_callback(json.dumps({
                "status": "error",
                "assistant_instruction": "No music description provided. Ask the user what kind of music they want.",
            }))
            return

        if bot_session.is_processing:
            await params.result_callback(json.dumps({
                "status": "busy",
                "assistant_instruction": "A generation is already in progress. Wait silently.",
            }))
            return

        # Credit check (3-layer: plan → topup → free)
        from src.bot.services.limit_checker import can_generate as check_limits, send_limit_popup
        limit_reason = check_limits(bot_session)
        if limit_reason:
            await send_limit_popup(bot_session, rtvi, limit_reason)
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": "The user has used all their music credits. Tell them to subscribe or buy a top-up pack.",
            }))
            return

        bot_session.is_processing = True
        music_id = f"music_{int(time.time())}_{uuid.uuid4().hex[:4]}"
        await _send_rtvi(task, {"type": "generation_started", "data": {"type": "music", "status": "in_progress"}})

        try:
            logger.info(f"MUSIC_GENERATE_START: session={session_id} prompt={prompt[:80]}")
            start_time = time.time()

            audio_bytes = await _generate_music(prompt)
            generation_time = time.time() - start_time
            logger.info(f"MUSIC_LYRIA_DONE: session={session_id} time={generation_time:.1f}s size={len(audio_bytes)}")

            audio_url = await _store_audio(audio_bytes, session_id, device_id)
            total_time = time.time() - start_time

            logger.info(
                f"MUSIC_GENERATED: session={session_id} music_id={music_id} "
                f"time={total_time:.1f}s url={audio_url}"
            )

            transcript_manager.add_generation("music", {
                "music_id": music_id,
                "prompt": prompt,
                "audio_url": audio_url,
                "duration_secs": AUDIO_DURATION_SECS,
                "total_time": round(total_time, 1),
            })

            # Deduct credit & track for session-end persist
            from src.bot.services.limit_checker import use_credit
            use_credit(bot_session)

            # Track music for session end
            bot_session.add_generated_music(
                metadata={
                    "music_id": music_id,
                    "audio_url": audio_url,
                    "duration_secs": AUDIO_DURATION_SECS,
                },
                prompt=prompt,
            )

            await _send_rtvi(task, {
                "type": "music_generation_complete",
                "data": {
                    "music_id": music_id,
                    "audio_url": audio_url,
                    "prompt": prompt,
                    "duration_secs": AUDIO_DURATION_SECS,
                },
            })

            await params.result_callback(json.dumps({
                "status": "completed",
                "music_id": music_id,
                "audio_url": audio_url,
                "assistant_instruction": "Music is ready! Tell the user their music track has been generated and they can listen to it.",
            }))

        except Exception as e:
            logger.error(f"MUSIC_ERROR: session={session_id} error={e}", exc_info=True)
            await _send_rtvi(task, {
                "type": "generation_failed",
                "data": {"error": str(e)[:100], "media_type": "music"},
            })
            await params.result_callback(json.dumps({
                "status": "error",
                "assistant_instruction": f"Music generation failed: {str(e)[:100]}. Apologize and suggest trying again.",
            }))
        finally:
            bot_session.is_processing = False

    return handle_generate_music

+            if limit_reason == "cooldown":

+                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_music again."

+                instruction = "The user has used all their music credits. Tell them to subscribe or buy a top-up pack."

-                "assistant_instruction": "The user has used all their music credits. Tell them to subscribe or buy a top-up pack.",


189	            return
190
191	        # Credit check (3-layer: plan → topup → free)
192	        from src.bot.services.limit_checker import can_generate as check_limits, send_limit_popup
193	        limit_reason = check_limits(bot_session)
194	        if limit_reason:
195	            await send_limit_popup(bot_session, rtvi, limit_reason)
196	            await params.result_callback(json.dumps({
197	                "status": "limit_reached",
198	                "assistant_instruction": "The user has used all their presentation credits. Tell them to subscribe or buy a top-up pack.",
199	            }))
200	            return
201
202	        bot_session.is_processing = True
203	        ppt_id = f"ppt_{int(time.time())}_{uuid.uuid4().hex[:4]}"

            return

        # Credit check (3-layer: plan → topup → free)
        from src.bot.services.limit_checker import can_generate as check_limits, send_limit_popup
        limit_reason = check_limits(bot_session)
        if limit_reason:
            await send_limit_popup(bot_session, rtvi, limit_reason)
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": "The user has used all their presentation credits. Tell them to subscribe or buy a top-up pack.",
            }))
            return

        bot_session.is_processing = True
        ppt_id = f"ppt_{int(time.time())}_{uuid.uuid4().hex[:4]}"


        limit_reason = check_limits(bot_session)
        if limit_reason:
            await send_limit_popup(bot_session, rtvi, limit_reason)
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": "The user has used all their presentation credits. Tell them to subscribe or buy a top-up pack.",
            }))

        limit_reason = check_limits(bot_session)
        if limit_reason:
            await send_limit_popup(bot_session, rtvi, limit_reason)
            if limit_reason == "cooldown":
                cooldown = bot_session.cooldown_minutes
                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_ppt again."
            else:
                instruction = "The user has used all their presentation credits. Tell them to subscribe or buy a top-up pack."
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": instruction,
            }))


The file /home/ubuntu/pipecat/src/bot/services/ppt_service.py has been updated successfully.

        limit_reason = check_limits(bot_session)
        if limit_reason:
            await send_limit_popup(bot_session, rtvi, limit_reason)
            if limit_reason == "cooldown":
                cooldown = bot_session.cooldown_minutes
                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_ppt again."
            else:
                instruction = "The user has used all their presentation credits. Tell them to subscribe or buy a top-up pack."
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": instruction,
            }))

"""Presentation generation service using Gamma API with GCS storage."""

import asyncio
import json
import os
import time
import uuid

import httpx
import fitz  # PyMuPDF
from loguru import logger

from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.frames.frames import OutputTransportMessageUrgentFrame
from pipecat.services.llm_service import FunctionCallParams

from src.core.storage import upload_to_s3

# --- Function Schema ---

generate_ppt_function = FunctionSchema(
    name="generate_ppt",
    description=(
        "Generate a presentation (PPT/PDF) from a text description using AI. "
        "The user describes the topic and you create a professional presentation. "
        "CRITICAL: Do NOT call this function again if generation is already in progress."
    ),
    properties={
        "topic": {
            "type": "string",
            "description": "The topic or detailed description for the presentation",
        },
        "num_slides": {
            "type": "integer",
            "description": "Number of slides to generate (default 8, max 15)",
        },
    },
    required=["topic"],
)

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from src.bot.core.config import BotSessionState, TranscriptManager

GAMMA_API_URL = "https://public-api.gamma.app/v1.0/generations"
GAMMA_API_KEY = os.getenv("GAMMA_API_KEY", "")

POLL_INTERVAL = 5  # seconds between status checks
POLL_TIMEOUT = 120  # max seconds to wait

# --- Helpers ---

async def _send_rtvi(task, data: dict):
    try:
        frame = OutputTransportMessageUrgentFrame(message={
            "label": "rtvi-ai", "type": "server-message", "data": data,
        })
        await task.queue_frame(frame)
    except Exception as e:
        logger.warning(f"Failed to send RTVI message: {e}")

async def _create_generation(topic: str, num_slides: int) -> str:
    """Start a Gamma generation and return the generationId."""
    api_key = GAMMA_API_KEY or os.getenv("GAMMA_API_KEY", "")
    if not api_key:
        raise Exception("GAMMA_API_KEY not set")

    async with httpx.AsyncClient(timeout=30.0) as client:
        response = await client.post(
            GAMMA_API_URL,
            headers={
                "Content-Type": "application/json",
                "X-API-KEY": api_key,
            },
            json={
                "inputText": topic,
                "textMode": "generate",
                "format": "presentation",
                "numCards": min(num_slides, 15),
                "exportAs": "pdf",
            },
        )
        response.raise_for_status()
        data = response.json()
        generation_id = data.get("generationId")
        if not generation_id:
            raise Exception(f"No generationId in response: {data}")
        return generation_id

async def _poll_generation(generation_id: str) -> dict:
    """Poll Gamma until generation completes. Returns the final response dict."""
    api_key = GAMMA_API_KEY or os.getenv("GAMMA_API_KEY", "")
    url = f"{GAMMA_API_URL}/{generation_id}"
    headers = {
        "Content-Type": "application/json",
        "X-API-KEY": api_key,
    }

    start = time.time()
    async with httpx.AsyncClient(timeout=30.0) as client:
        while time.time() - start < POLL_TIMEOUT:
            response = await client.get(url, headers=headers)
            response.raise_for_status()
            data = response.json()
            status = data.get("status", "")

            if status == "completed":
                return data
            elif status in ("failed", "error"):
                raise Exception(f"Gamma generation failed: {data}")

            await asyncio.sleep(POLL_INTERVAL)

    raise Exception(f"Gamma generation timed out after {POLL_TIMEOUT}s")

async def _download_and_store_pdf(export_url: str, session_id: str, device_id: str) -> tuple:
    """Download PDF from Gamma, extract first page as thumbnail, upload both to GCS.
    Returns (pdf_url, thumbnail_url)."""
    async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
        response = await client.get(export_url)
        response.raise_for_status()
        pdf_bytes = response.content

    logger.info(f"PPT_PDF_DOWNLOADED: session={session_id} size={len(pdf_bytes)}")

    # Upload PDF to GCS
    pdf_key = f"{device_id}/{session_id}/ppt/{uuid.uuid4().hex[:12]}.pdf"
    pdf_url = await upload_to_s3(pdf_bytes, pdf_key, "application/pdf")

    # Extract first page as thumbnail using PyMuPDF
    thumbnail_url = ""
    try:
        doc = fitz.open(stream=pdf_bytes, filetype="pdf")
        if doc.page_count > 0:
            page = doc[0]
            mat = fitz.Matrix(2, 2)
            pix = page.get_pixmap(matrix=mat)
            thumb_bytes = pix.tobytes("png")
            doc.close()

            thumb_key = f"{device_id}/{session_id}/ppt_thumb/{uuid.uuid4().hex[:12]}.png"
            thumbnail_url = await upload_to_s3(thumb_bytes, thumb_key, "image/png")
            logger.info(f"PPT_THUMBNAIL_CREATED: session={session_id} size={len(thumb_bytes)}")
        else:
            doc.close()
    except Exception as e:
        logger.warning(f"PPT_THUMBNAIL_FAILED: session={session_id} error={e}")

    return pdf_url, thumbnail_url

# --- Handler factory ---

def create_ppt_handler(
    session_state,
    rtvi,
    bot_session: "BotSessionState",
    transcript_manager: "TranscriptManager",
    task,
    llm,
):
    """Create presentation generation handler."""

    async def handle_generate_ppt(params: FunctionCallParams):
        args = params.arguments
        topic = args.get("topic", "")
        num_slides = args.get("num_slides", 8)
        session_id = bot_session.session_id or "unknown"
        device_id = bot_session.device_id or "common"

        if not topic:
            await params.result_callback(json.dumps({
                "status": "error",
                "assistant_instruction": "No topic provided. Ask the user what presentation they want.",
            }))
            return

        if bot_session.is_processing:
            await params.result_callback(json.dumps({
                "status": "busy",
                "assistant_instruction": "A generation is already in progress. Wait silently.",
            }))
            return

        # Credit check (3-layer: plan → topup → free)
        from src.bot.services.limit_checker import can_generate as check_limits, send_limit_popup
        limit_reason = check_limits(bot_session)
        if limit_reason:
            await send_limit_popup(bot_session, rtvi, limit_reason)
            await params.result_callback(json.dumps({
                "status": "limit_reached",
                "assistant_instruction": "The user has used all their presentation credits. Tell them to subscribe or buy a top-up pack.",
            }))
            return

        bot_session.is_processing = True
        ppt_id = f"ppt_{int(time.time())}_{uuid.uuid4().hex[:4]}"
        await _send_rtvi(task, {"type": "generation_started", "data": {"type": "ppt", "status": "in_progress"}})

        try:
            logger.info(f"PPT_GENERATE_START: session={session_id} topic={topic[:60]} slides={num_slides}")
            start_time = time.time()

            generation_id = await _create_generation(topic, num_slides)
            logger.info(f"PPT_GENERATION_CREATED: session={session_id} generation_id={generation_id}")

            result = await _poll_generation(generation_id)

            export_url = result.get("exportUrl", "")
            gamma_url = result.get("gammaUrl", "")
            gamma_id = result.get("gammaId", "")

            pdf_url, thumbnail_url = await _download_and_store_pdf(export_url, session_id, device_id)

            total_time = time.time() - start_time
            logger.info(
                f"PPT_GENERATED: session={session_id} ppt_id={ppt_id} "
                f"gamma_id={gamma_id} time={total_time:.1f}s pdf={pdf_url}"
            )

            transcript_manager.add_generation("ppt", {
                "ppt_id": ppt_id,
                "topic": topic,
                "gamma_id": gamma_id,
                "gamma_url": gamma_url,
                "pdf_url": pdf_url,
                "thumbnail_url": thumbnail_url,
                "export_url": export_url,
                "total_time": round(total_time, 1),
            })

            # Deduct credit & track for session-end persist
            from src.bot.services.limit_checker import use_credit
            use_credit(bot_session)

            # Track PPT for session_ppts table
            bot_session.add_generated_ppt(
                metadata={
                    "ppt_id": ppt_id,
                    "pdf_url": pdf_url,
                    "thumbnail_url": thumbnail_url,
                    "gamma_id": gamma_id,
                    "gamma_url": gamma_url,
                },
                topic=topic
            )

            await _send_rtvi(task, {
                "type": "ppt_generation_complete",
                "data": {
                    "ppt_id": ppt_id,
                    "pdf_url": pdf_url,
                    "thumbnail_url": thumbnail_url,
                    "topic": topic,
                },
            })

            await params.result_callback(json.dumps({
                "status": "completed",
                "ppt_id": ppt_id,
                "pdf_url": pdf_url,
                "thumbnail_url": thumbnail_url,
                "assistant_instruction": "Presentation is ready! Tell the user their presentation has been generated and they can download the PDF.",
            }))

        except Exception as e:
            logger.error(f"PPT_ERROR: session={session_id} error={e}", exc_info=True)
            await _send_rtvi(task, {
                "type": "generation_failed",
                "data": {"error": str(e)[:100], "media_type": "ppt"},
            })
            await params.result_callback(json.dumps({
                "status": "error",
                "assistant_instruction": f"Presentation generation failed: {str(e)[:100]}. Apologize and suggest trying again.",
            }))
        finally:
            bot_session.is_processing = False

    return handle_generate_ppt

+                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_ppt again."

+                instruction = "The user has used all their presentation credits. Tell them to subscribe or buy a top-up pack."

-                "assistant_instruction": "The user has used all their presentation credits. Tell them to subscribe or buy a top-up pack.",


Now also update the `/check-limits` API in `usage_service.py`:


    # Blocked — show payment options filtered by provider
    # Apply country-level price overrides for global users
    country_prices = {}
    if country and region != "india":
        from src.repository.country_prices_repository import country_prices_repo
        country_prices = await country_prices_repo.get_prices_for_country(country)

    if sub:

    # Blocked — check if payments are enabled
    from src.repository.config_v2_repository import config_v2_repo
    payments_config = await config_v2_repo.get_payments_config()
    if not payments_config.get("payments_enabled", True):
        cooldowns = payments_config.get("cooldowns", {})
        cooldown_minutes = cooldowns.get(content_type, 30)
        return {
            "allowed": False,
            "reason": "cooldown",
            "content_type": content_type,
            "persona_id": persona_id,
            "cooldown_minutes": cooldown_minutes,
        }

    # Show payment options filtered by provider
    # Apply country-level price overrides for global users
    country_prices = {}
    if country and region != "india":
        from src.repository.country_prices_repository import country_prices_repo
        country_prices = await country_prices_repo.get_prices_for_country(country)

    if sub:


The file /home/ubuntu/pipecat/src/service/usage_service.py has been updated successfully.

    # Blocked — check if payments are enabled
    from src.repository.config_v2_repository import config_v2_repo
    payments_config = await config_v2_repo.get_payments_config()
    if not payments_config.get("payments_enabled", True):
        cooldowns = payments_config.get("cooldowns", {})
        cooldown_minutes = cooldowns.get(content_type, 30)
        return {
            "allowed": False,
            "reason": "cooldown",
            "content_type": content_type,
            "persona_id": persona_id,
            "cooldown_minutes": cooldown_minutes,
        }

    # Show payment options filtered by provider
    # Apply country-level price overrides for global users
    country_prices = {}
    if country and region != "india":
        from src.repository.country_prices_repository import country_prices_repo
        country_prices = await country_prices_repo.get_prices_for_country(country)

    if sub:

"""Service layer for usage state, generation checks, and plan assignment."""

import hashlib
from typing import Dict, Any, List

from loguru import logger

from src.repository.plan_repository import plan_repo
from src.repository.subscription_repository import subscription_repo
from src.repository.topup_repository import topup_repo
from src.repository.usage_repository import usage_repo

def _device_hash(device_id: str) -> int:
    """Deterministic 0-99 hash for A/B assignment."""
    return int(hashlib.sha256(device_id.encode()).hexdigest(), 16) % 100

async def _get_device_info(device_id: str) -> Dict[str, Any]:
    """Get device region and country, defaulting to india for legacy devices."""
    from src.repository.device_repository import device_repo
    device = await device_repo.get_device(device_id)
    if device:
        return {
            "region": device.get("region") or "india",
            "country": device.get("country"),
        }
    return {"region": "india", "country": None}

async def _get_device_region(device_id: str) -> str:
    """Get device region, defaulting to 'india' for legacy devices."""
    info = await _get_device_info(device_id)
    return info["region"]

def _get_payment_provider(region: str) -> str:
    """Map region to payment provider."""
    return "google_play" if region == "global" else "razorpay"

async def get_usage_state(device_id: str, content_type: str, payment_provider: str = None) -> Dict[str, Any]:
    """Build usage state for a device + content type (plan/topup level).

    Free limits are persona-level now, handled separately in can_generate_persona().
    Subscription and topup repos already handle 'all' fallback internally.
    If payment_provider is given, only checks subs from that provider.
    """
    sub = await subscription_repo.get_active(device_id, content_type, payment_provider)
    plan_remaining = sub["credits_remaining"] if sub else 0
    subscription_id = sub["id"] if sub else None
    plan_content_type = sub["plan_content_type"] if sub else None

    topup_remaining = await topup_repo.get_credits(device_id, content_type, payment_provider)

    return {
        "plan_remaining": plan_remaining,
        "topup_remaining": topup_remaining,
        "has_plan": sub is not None,
        "subscription_id": subscription_id,
        "plan_content_type": plan_content_type,
    }

async def can_generate_persona(device_id: str, persona_id: str, payment_provider: str = None) -> Dict[str, Any]:
    """Check if the device can use a persona.

    Credit check order: plan → topup → region free limit (A/B) → persona free limit.

    For global users, region_free_limits with ab_percentage controls what fraction
    of users get free access. Users outside the A/B percentage see the paywall.

    Args:
        device_id: The device identifier.
        persona_id: The persona to check.
        payment_provider: If set, filters plans/topups shown when blocked.

    Returns:
        {"allowed": True} — allowed
        {"allowed": False, "reason": "subscribe"|"topup", ...} — blocked
    """
    from src.repository.persona_repository import persona_repo

    persona = await persona_repo.get_persona_by_id(persona_id)
    if not persona:
        return {"allowed": True}

    content_type = persona.get("content_type", "")

    # Resolve region, country, and payment provider if not provided
    device_info = await _get_device_info(device_id)
    region = device_info["region"]
    country = device_info["country"]
    if not payment_provider:
        payment_provider = _get_payment_provider(region)

    # 1. Check plan credits (content_type level, with 'all' fallback in repo)
    sub = await subscription_repo.get_active(device_id, content_type, payment_provider)
    plan_remaining = sub["credits_remaining"] if sub else 0
    if plan_remaining > 0:
        return {"allowed": True}

    # 2. Check topup credits (content_type level, with 'all' fallback in repo)
    topup_remaining = await topup_repo.get_credits(device_id, content_type, payment_provider)
    if topup_remaining > 0:
        return {"allowed": True}

    # 3. Free limits: India uses persona-level (preserves old behavior),
    #    Global uses region_free_limits with A/B testing.
    if region == "india":
        # India: always use persona-level free limits
        free_count = persona.get("free_count", 0)
        free_window_hours = persona.get("free_window_hours", 24)
        if free_count > 0:
            used = await usage_repo.count_persona_generations(device_id, persona_id, content_type, free_window_hours)
            if used < free_count:
                return {"allowed": True}
    else:
        # Global: use region_free_limits with A/B percentage
        # Group A (hash < ab_percentage): fully free, no limits, no paywall
        # Group B (hash >= ab_percentage): gets free_count free, then paywall
        region_limits = await plan_repo.get_region_free_limits(region)
        region_limit = region_limits.get(content_type)
        if region_limit:
            ab_pct = region_limit.get("ab_percentage", 100)
            device_h = _device_hash(device_id)

            if device_h < ab_pct:
                # Group A: fully free — always allowed
                return {"allowed": True}
            else:
                # Group B: gets free_count free, then paywall
                free_count = region_limit["free_count"]
                free_window = region_limit["free_window_hours"]
                if free_count > 0:
                    used = await usage_repo.count_persona_generations(device_id, persona_id, content_type, free_window)
                    if used < free_count:
                        return {"allowed": True}
        else:
            # No region limits configured — fall back to persona-level
            free_count = persona.get("free_count", 0)
            free_window_hours = persona.get("free_window_hours", 24)
            if free_count > 0:
                used = await usage_repo.count_persona_generations(device_id, persona_id, content_type, free_window_hours)
                if used < free_count:
                    return {"allowed": True}

    # Blocked — show payment options filtered by provider
    # Apply country-level price overrides for global users
    country_prices = {}
    if country and region != "india":
        from src.repository.country_prices_repository import country_prices_repo
        country_prices = await country_prices_repo.get_prices_for_country(country)

    if sub:
        topup = await get_topup_for_device(device_id, content_type, payment_provider)
        if country_prices and topup:
            from src.repository.country_prices_repository import country_prices_repo
            topup = country_prices_repo.apply_country_price(topup, country_prices)
        return {
            "allowed": False,
            "reason": "topup",
            "content_type": content_type,
            "persona_id": persona_id,
            "plan": topup,
        }
    else:
        plan = await get_plans_for_device(device_id, content_type, payment_provider)
        if country_prices and plan:
            from src.repository.country_prices_repository import country_prices_repo
            plan = country_prices_repo.apply_country_price(plan, country_prices)
        return {
            "allowed": False,
            "reason": "subscribe",
            "content_type": content_type,
            "persona_id": persona_id,
            "plan": plan,
        }

def _assign_plan(device_id: str, plans: List[Dict[str, Any]]) -> Dict[str, Any]:
    """Deterministically assign a plan variant to a device using weighted hash."""
    if not plans:
        return {}
    h = _device_hash(device_id)
    cumulative = 0
    for p in plans:
        cumulative += p.get("percentage", 0)
        if h < cumulative:
            plan = dict(p)
            plan.pop("percentage", None)
            return plan
    plan = dict(plans[-1])
    plan.pop("percentage", None)
    return plan

async def get_plans_for_device(device_id: str, content_type: str, payment_provider: str = None) -> Dict[str, Any]:
    """Get the A/B-assigned plan for a device + content type.

    Tries specific content_type first, falls back to 'all' if no plans found.
    """
    plans = await plan_repo.get_plans_for_content(content_type, payment_provider)
    if not plans and content_type != "all":
        plans = await plan_repo.get_plans_for_content("all", payment_provider)
    return _assign_plan(device_id, plans)

async def get_topup_for_device(device_id: str, content_type: str, payment_provider: str = None) -> Dict[str, Any]:
    """Get the A/B-assigned topup for a device + content type.

    Tries specific content_type first, falls back to 'all' if no topups found.
    """
    topups = await plan_repo.get_topups_for_content(content_type, payment_provider)
    if not topups and content_type != "all":
        topups = await plan_repo.get_topups_for_content("all", payment_provider)
    return _assign_plan(device_id, topups)

async def get_topups_for_content(content_type: str, payment_provider: str = None) -> List[Dict[str, Any]]:
    """Get available topup packs for a content type."""
    return await plan_repo.get_topups_for_content(content_type, payment_provider)

async def get_all_usage(device_id: str) -> Dict[str, Dict[str, Any]]:
    """Get usage state for all content types."""
    content_types = ["image_gen", "video_gen", "ad_video", "ppt_gen", "music_gen"]
    result = {}
    for ct in content_types:
        result[ct] = await get_usage_state(device_id, ct)
    return result

-    # Blocked — show payment options filtered by provider

+    # Blocked — check if payments are enabled

+    from src.repository.config_v2_repository import config_v2_repo

+    payments_config = await config_v2_repo.get_payments_config()

+    if not payments_config.get("payments_enabled", True):

+        cooldowns = payments_config.get("cooldowns", {})

+        cooldown_minutes = cooldowns.get(content_type, 30)

+            "content_type": content_type,

+            "cooldown_minutes": cooldown_minutes,

+    # Show payment options filtered by provider


Now let me insert the config into DB and deploy. First the DB insert:


Bash

kubectl exec -it $(kubectl get pods -l app=maya-pipecat-v5 --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') -- python3 -c "
import requests, json, os, time
url = os.environ.get('TURSO_DATABASE_URL', '').replace('libsql://', 'https://') + '/v2/pipeline'
token = os.environ.get('TURSO_AUTH_TOKEN', '')
config = json.dumps({
    'payments_enabled': False,
    'cooldowns': {
        'image_gen': 30,
        'video_gen': 60,
        'music_gen': 60,
        'ppt_gen': 60
    }
})
resp = requests.post(url, headers={
    'Authorization': f'Bearer {token}',
    'Content-Type': 'application/json'
}, json={
    'requests': [{
        'type': 'execute',
        'stmt': {
            'sql': 'INSERT OR REPLACE INTO configurations_v2 (config_key, config_value, updated_at) VALUES (?, ?, ?)',
            'args': [
                {'type': 'text', 'value': 'payments_config'},
                {'type': 'text', 'value': config},
                {'type': 'integer', 'value': str(int(time.time()))}
            ]
        }
    }]
})
print(resp.status_code, resp.text[:200])
"

Insert payments_config into DB


Unable to use a TTY - input is not a terminal or the right kind of file
200 {"baton":null,"base_url":null,"results":[{"type":"ok","response":{"type":"execute","result":{"cols":[],"rows":[],"affected_row_count":1,"last_insert_rowid":"24","replication_index":null,"rows_read":0,


Config inserted. Now build and deploy:


docker build -t asia-south1-docker.pkg.dev/mayaresearchgcp/maya-pipecat/maya-pipecat-v5:latest . 2>&1 | tail -5

Build Docker image


#19 exporting to image
#19 exporting layers 0.1s done
#19 writing image sha256:6edbdd0e58f3f548cbcc0d542a9ca6b1940a3b8fa81112a88ab3b90b3bb6d1c9 done
#19 naming to asia-south1-docker.pkg.dev/mayaresearchgcp/maya-pipecat/maya-pipecat-v5:latest done
#19 DONE 0.1s


docker push asia-south1-docker.pkg.dev/mayaresearchgcp/maya-pipecat/maya-pipecat-v5:latest 2>&1 | tail -5

Push Docker image


4c80003eae39: Layer already exists
0176e4be5cbf: Layer already exists
6143ee9e3de0: Layer already exists
7efef9ce656b: Pushed
latest: digest: sha256:85eb0062765951b3365dcbfd28a59bc03e40f65e0559e95f12edaaf90c5d8a17 size: 3670


kubectl rollout restart deployment maya-pipecat-v5

Rollout restart


deployment.apps/maya-pipecat-v5 restarted


Done. Here's the summary:

**DB config** (`payments_config` in `configurations_v2`):
```json
{
  "payments_enabled": false,
  "cooldowns": {
    "image_gen": 30,
    "video_gen": 60,
    "music_gen": 60,
    "ppt_gen": 60
  }
}
```

**How it works:**
- Persona table `free_count` / `free_window_hours` still controls how many free generations per window
- When limits hit + `payments_enabled: false` → agent says "limit khatam, X minutes baad try karo" + frontend gets `show_cooldown` event
- When limits hit + `payments_enabled: true` → shows payment popup (original behavior)
- Cooldowns are per content type (image=30min, video/music/ppt=60min)
- Config cached 120s in Redis, so changes take effect within 2 minutes

To toggle payments back on, just update the DB:
```sql
UPDATE configurations_v2 SET config_value = '{"payments_enabled": true, "cooldowns": {...}}' WHERE config_key = 'payments_config'
```


check changes u added


Check changed files


 src/bot/core/config.py                 |  4 ++++
 src/bot/core/main.py                   | 12 ++++++++++--
 src/bot/services/image_service.py      |  7 ++++++-
 src/bot/services/limit_checker.py      | 31 ++++++++++++++++++++++++-------
 src/bot/services/music_service.py      |  7 ++++++-
 src/bot/services/ppt_service.py        |  7 ++++++-
 src/bot/services/video_service.py      | 14 ++++++++++++--
 src/repository/config_v2_repository.py | 31 +++++++++++++++++++++++++++++++
 src/service/usage_service.py           | 16 +++++++++++++++-
 9 files changed, 114 insertions(+), 15 deletions(-)


Show full diff


diff --git a/src/bot/core/config.py b/src/bot/core/config.py
index 7e0ff51..2109115 100644
--- a/src/bot/core/config.py
+++ b/src/bot/core/config.py
@@ -185,6 +185,10 @@ class BotSessionState:
         self.credits_used_topup: int = 0  # track for session-end persist
         self.session_saved: bool = False  # flag to prevent double-saving

+        # Payments config (loaded from DB at session start)
+        self.payments_enabled: bool = True  # True = show payment popup, False = cooldown message
+        self.cooldown_minutes: int = 30     # default cooldown when payments off
+
     def get_session_info(self) -> str:
         """Get formatted session info for logging."""
         info = f"session_id={self.session_id} device_id={self.device_id}"
diff --git a/src/bot/core/main.py b/src/bot/core/main.py
index 801cadd..3f03fa4 100644
--- a/src/bot/core/main.py
+++ b/src/bot/core/main.py
@@ -126,7 +126,14 @@ async def run_bot_webrtc_global_agent(webrtc_connection, device_id: str = None,
         else:
             bot_session.free_remaining = 0

-        logger.info(f"USAGE_STATE: session_id={session_id} device_id={device_id} persona={persona_id} content={bot_session.content_type} free={bot_session.free_remaining}/{free_count} plan={bot_session.plan_remaining} topup={bot_session.topup_remaining} transport={transport_type}")
+        # Load payments config
+        from src.repository.config_v2_repository import config_v2_repo
+        payments_config = await config_v2_repo.get_payments_config()
+        bot_session.payments_enabled = payments_config.get("payments_enabled", True)
+        cooldowns = payments_config.get("cooldowns", {})
+        bot_session.cooldown_minutes = cooldowns.get(bot_session.content_type, 30)
+
+        logger.info(f"USAGE_STATE: session_id={session_id} device_id={device_id} persona={persona_id} content={bot_session.content_type} free={bot_session.free_remaining}/{free_count} plan={bot_session.plan_remaining} topup={bot_session.topup_remaining} payments={bot_session.payments_enabled} cooldown={bot_session.cooldown_minutes}m transport={transport_type}")

     # Determine which tools to enable
     video_enabled = "generate_video" in persona_tools or "generate_video_from_image" in persona_tools
@@ -135,8 +142,9 @@ async def run_bot_webrtc_global_agent(webrtc_connection, device_id: str = None,
     music_enabled = "generate_music" in persona_tools
     bot_session.video_enabled = video_enabled

-    use_gemini_live = True
+    use_gemini_live = language not in (None, "hi")
     bot_session.use_gemini_live = use_gemini_live
+    logger.info(f"PIPELINE_MODE: language={language} use_gemini_live={use_gemini_live}")

     vad_analyzer = create_vad_analyzer(session_id)
     bot_session.vad_analyzer = vad_analyzer
diff --git a/src/bot/services/image_service.py b/src/bot/services/image_service.py
index baf5aaa..27726c7 100644
--- a/src/bot/services/image_service.py
+++ b/src/bot/services/image_service.py
@@ -168,9 +168,14 @@ def create_cf_image_handler(
             block_reason = can_generate(bot_session)
             if block_reason:
                 await send_limit_popup(bot_session, rtvi_processor, block_reason)
+                if block_reason == "cooldown":
+                    cooldown = bot_session.cooldown_minutes
+                    instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call cf_image again."
+                else:
+                    instruction = "User has reached their generation limit. Tell them politely. Do NOT call cf_image again."
                 await params.result_callback(json.dumps({
                     "status": "limit_reached",
-                    "assistant_instruction": "User has reached their generation limit. Tell them politely. Do NOT call cf_image again.",
+                    "assistant_instruction": instruction,
                 }))
                 return

diff --git a/src/bot/services/limit_checker.py b/src/bot/services/limit_checker.py
index 583e186..91256d4 100644
--- a/src/bot/services/limit_checker.py
+++ b/src/bot/services/limit_checker.py
@@ -8,8 +8,9 @@ def can_generate(bot_session) -> str:

     Returns:
         "" — allowed
-        "subscribe" — no plan, free exhausted → show plans popup
-        "topup" — has plan but exhausted → show topup popup
+        "subscribe" — no plan, free exhausted, payments ON → show plans popup
+        "topup" — has plan but exhausted, payments ON → show topup popup
+        "cooldown" — limits exhausted, payments OFF → agent says wait
     """
     if bot_session.plan_remaining > 0:
         return ""
@@ -18,7 +19,10 @@ def can_generate(bot_session) -> str:
     if bot_session.free_remaining > 0:
         return ""

-    # Blocked
+    # Blocked — check if payments are enabled
+    if not bot_session.payments_enabled:
+        return "cooldown"
+
     if bot_session.subscription_id:
         return "topup"
     return "subscribe"
@@ -40,17 +44,31 @@ def use_credit(bot_session):

 async def send_limit_popup(bot_session, rtvi_processor, block_reason: str):
-    """Send limit reached popup with plan/topup data to the frontend.
+    """Send limit reached popup or cooldown message to the frontend.

-    Fetches plan data from DB so the pricing screen shows actual prices.
+    If block_reason is "cooldown", sends a cooldown message instead of payment popup.
+    The agent will verbally tell the user to wait.
     """
     from src.service.usage_service import get_plans_for_device, get_topup_for_device, get_topups_for_content

     content_type = bot_session.content_type or "image_gen"
     device_id = bot_session.device_id

-    # Determine payment provider from region
+    if block_reason == "cooldown":
+        cooldown_minutes = bot_session.cooldown_minutes
+        logger.info(f"COOLDOWN_LIMIT: device={device_id} content={content_type} cooldown_minutes={cooldown_minutes}")
+        await rtvi_processor.send_server_message({
+            "type": "show_cooldown",
+            "data": {
+                "content_type": content_type,
+                "cooldown_minutes": cooldown_minutes,
+            },
+        })
+        return
+
+    # Payment popup flow (existing behavior)
     from src.service.usage_service import _get_device_region, _get_payment_provider
+
     region = await _get_device_region(device_id)
     payment_provider = _get_payment_provider(region)

@@ -75,6 +93,5 @@ async def send_limit_popup(bot_session, rtvi_processor, block_reason: str):
         logger.info(f"LIMIT_POPUP_SENT: device={device_id} reason={block_reason} content={content_type} provider={payment_provider} plan_id={plan_data.get('plan_id') or plan_data.get('topup_id', 'none')}")
     except Exception as e:
         logger.error(f"LIMIT_POPUP_FAILED: device={device_id} error={e}")
-        # Fallback: send without data (old behavior)
         popup_type = "show_plans_popup" if block_reason == "subscribe" else "show_topup_popup"
         await rtvi_processor.send_server_message({"type": popup_type, "content_type": content_type})
diff --git a/src/bot/services/music_service.py b/src/bot/services/music_service.py
index dc0af11..eaca0fa 100644
--- a/src/bot/services/music_service.py
+++ b/src/bot/services/music_service.py
@@ -167,9 +167,14 @@ def create_music_handler(
         limit_reason = check_limits(bot_session)
         if limit_reason:
             await send_limit_popup(bot_session, rtvi, limit_reason)
+            if limit_reason == "cooldown":
+                cooldown = bot_session.cooldown_minutes
+                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_music again."
+            else:
+                instruction = "The user has used all their music credits. Tell them to subscribe or buy a top-up pack."
             await params.result_callback(json.dumps({
                 "status": "limit_reached",
-                "assistant_instruction": "The user has used all their music credits. Tell them to subscribe or buy a top-up pack.",
+                "assistant_instruction": instruction,
             }))
             return

diff --git a/src/bot/services/ppt_service.py b/src/bot/services/ppt_service.py
index 71fb9de..748fbc6 100644
--- a/src/bot/services/ppt_service.py
+++ b/src/bot/services/ppt_service.py
@@ -193,9 +193,14 @@ def create_ppt_handler(
         limit_reason = check_limits(bot_session)
         if limit_reason:
             await send_limit_popup(bot_session, rtvi, limit_reason)
+            if limit_reason == "cooldown":
+                cooldown = bot_session.cooldown_minutes
+                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_ppt again."
+            else:
+                instruction = "The user has used all their presentation credits. Tell them to subscribe or buy a top-up pack."
             await params.result_callback(json.dumps({
                 "status": "limit_reached",
-                "assistant_instruction": "The user has used all their presentation credits. Tell them to subscribe or buy a top-up pack.",
+                "assistant_instruction": instruction,
             }))
             return

diff --git a/src/bot/services/video_service.py b/src/bot/services/video_service.py
index 5d63da5..1b07d06 100644
--- a/src/bot/services/video_service.py
+++ b/src/bot/services/video_service.py
@@ -209,9 +209,14 @@ def create_video_handler(
         block_reason = _can_gen(bot_session)
         if block_reason:
             await send_limit_popup(bot_session, rtvi_processor, block_reason)
+            if block_reason == "cooldown":
+                cooldown = bot_session.cooldown_minutes
+                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_video again."
+            else:
+                instruction = "User has reached their generation limit. Tell them politely. Do NOT call generate_video again."
             await params.result_callback(json.dumps({
                 "status": "limit_reached",
-                "assistant_instruction": "User has reached their generation limit. Tell them politely. Do NOT call generate_video again.",
+                "assistant_instruction": instruction,
             }))
             return

@@ -268,9 +273,14 @@ def create_video_from_image_handler(
         block_reason = _can_gen(bot_session)
         if block_reason:
             await send_limit_popup(bot_session, rtvi_processor, block_reason)
+            if block_reason == "cooldown":
+                cooldown = bot_session.cooldown_minutes
+                instruction = f"User ka limit khatam ho gaya hai. Unko politely batao ki {cooldown} minutes baad try karo. Do NOT call generate_video_from_image again."
+            else:
+                instruction = "User has reached their generation limit. Tell them politely. Do NOT call generate_video_from_image again."
             await params.result_callback(json.dumps({
                 "status": "limit_reached",
-                "assistant_instruction": "User has reached their generation limit. Tell them politely. Do NOT call generate_video_from_image again.",
+                "assistant_instruction": instruction,
             }))
             return

diff --git a/src/repository/config_v2_repository.py b/src/repository/config_v2_repository.py
index f7a8543..dc2ffa3 100644
--- a/src/repository/config_v2_repository.py
+++ b/src/repository/config_v2_repository.py
@@ -189,6 +189,37 @@ class ConfigV2Repository(BaseRepository):
             return result
         return {"enabled": False, "percentage": 0, "max_queue": 3}

+    async def get_payments_config(self) -> Dict[str, Any]:
+        """Get payments config (toggle payments + per-content cooldowns).
+
+        DB value (JSON): {
+            "payments_enabled": false,
+            "cooldowns": {"image_gen": 30, "video_gen": 60, "music_gen": 60, "ppt_gen": 60}
+        }
+        - payments_enabled: true = show payment popup when limits hit, false = show cooldown message
+        - cooldowns: per content_type cooldown in minutes (agent tells user to wait)
+        """
+        from src.core.cache_layer import cached
+
+        async def _fetch():
+            try:
+                result = await self.query({
+                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
+                    "args": [{"type": "text", "value": "payments_config"}],
+                }, "config_v2_payments")
+                if result.rows:
+                    raw = self.extract_text(result.rows[0], 0)
+                    if raw:
+                        return json.loads(raw)
+            except Exception as e:
+                logger.error(f"CONFIG_V2_PAYMENTS_ERROR: {e}")
+            return None
+
+        result = await cached(key="config_v2:payments_config", fallback=_fetch, ttl=120)
+        if result:
+            return result
+        return {"payments_enabled": True, "cooldowns": {}}
+
     async def get_language_intro(self, lang_code: str, persona_id: str) -> str:
         """Get language-specific intro for a persona."""
         from src.core.cache_layer import cached
diff --git a/src/service/usage_service.py b/src/service/usage_service.py
index 528daf2..79b187c 100644
--- a/src/service/usage_service.py
+++ b/src/service/usage_service.py
@@ -145,7 +145,21 @@ async def can_generate_persona(device_id: str, persona_id: str, payment_provider
                 if used < free_count:
                     return {"allowed": True}

-    # Blocked — show payment options filtered by provider
+    # Blocked — check if payments are enabled
+    from src.repository.config_v2_repository import config_v2_repo
+    payments_config = await config_v2_repo.get_payments_config()
+    if not payments_config.get("payments_enabled", True):
+        cooldowns = payments_config.get("cooldowns", {})
+        cooldown_minutes = cooldowns.get(content_type, 30)
+        return {
+            "allowed": False,
+            "reason": "cooldown",
+            "content_type": content_type,
+            "persona_id": persona_id,
+            "cooldown_minutes": cooldown_minutes,
+        }
+
+    # Show payment options filtered by provider
     # Apply country-level price overrides for global users
     country_prices = {}
     if country and region != "india":


All changes across 9 files:

| File | Change |
|------|--------|
| `config.py` | Added `payments_enabled` + `cooldown_minutes` to BotSessionState |
| `main.py` | Load `payments_config` from DB at session start + Hindi pipeline fix |
| `config_v2_repository.py` | Added `get_payments_config()` with 120s cache |
| `limit_checker.py` | Returns `"cooldown"` when payments off; sends `show_cooldown` event instead of payment popup |
| `usage_service.py` | `/check-limits` API also returns cooldown when payments off |
| `image_service.py` | Agent says cooldown message with minutes |
| `video_service.py` | Same for both t2v and i2v handlers |
| `music_service.py` | Same for music |
| `ppt_service.py` | Same for ppt |


can u check how many pods are running and how much cluster we using


kubectl get pods -l app=maya-pipecat-v5 --no-headers | awk '{print $3}' | sort | uniq -c | sort -rn

Count pods by status


Check node resource usage


     80 Running
      4 ContainerCreating

     80 Running
      4 ContainerCreating


NAME                                          CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)
gke-maya-pipecat-default-pool-f76748ed-24sr   16458m       25%      35640Mi         15%
gke-maya-pipecat-default-pool-f76748ed-l70x   18226m       28%      42716Mi         18%
gke-maya-pipecat-default-pool-f76748ed-trnd   18247m       28%      36373Mi         15%
gke-maya-pipecat-default-pool-f76748ed-vks4   17174m       26%      39626Mi         17%
gke-maya-pipecat-pool-v2-ca055238-s01j        16934m       26%      37999Mi         16%
gke-maya-pipecat-pool-v2-ca055238-xw71        16570m       25%      34605Mi         15%


**84 pods** (80 running, 4 creating)

**6 nodes** — resource usage:

| Node | CPU | CPU % | Memory | Mem % |
|------|-----|-------|--------|-------|
| `..24sr` | 16.5 cores | 25% | 35 GB | 15% |
| `..l70x` | 18.2 cores | 28% | 42 GB | 18% |
| `..trnd` | 18.2 cores | 28% | 36 GB | 15% |
| `..vks4` | 17.2 cores | 26% | 40 GB | 17% |
| `..s01j` (new) | 16.9 cores | 26% | 37 GB | 16% |
| `..xw71` (new) | 16.6 cores | 25% | 35 GB | 15% |

**Totals:** ~104 / 384 vCPUs (27% CPU), ~226 GB / 1.4 TB RAM (16% memory)

Cluster is very underutilized right now — the rollout just completed so pods are fresh with low load. Memory went from 80% (4 nodes) to 16% (6 nodes). You have a lot of headroom now.


people are saying maya voice cutting ..not talking correctly .. all hindi useers what may be issue


This is likely because we just switched Hindi users from Gemini Live (single service doing STT+LLM+TTS) back to the traditional pipeline. Let me check the logs.


kubectl logs $(kubectl get pods -l app=maya-pipecat-v5 --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') --tail=200 2>&1 | grep -i "PIPELINE_MODE\|ERROR\|FAILED\|TTS_\|STT_\|language=hi" | tail -30

Check logs for Hindi pipeline issues