"""Provide modal runner for ChatGPT functionality, including prompt generation and moderation.
It spins up two stubs: ChatGptStub and ConductorStub.
ChatGptStub is responsible for moderating prompts and lyrics, and generating prompts from gpt_description_prompts.
ConductorStub is responsible for distributing queue items to the appropriate model worker.
"""

import json
import logging
import random
import os
import time
from collections import Counter
from collections.abc import Callable
from typing import Any, Dict, Optional

import modal
from openai import OpenAI  # type: ignore[attr-defined]

from suno_utils.worker.settings import bedrock_client
from suno_utils.gpt import chirp_v2
from suno_utils.utils.image_extraction import ImageExtractionError, ImageModerationFailure
from suno_utils.utils.constants import SUNO_AD
from suno_utils.worker.generate_song_lyrics import (
    LyricsLength,
    ModerationError,
    ModerationFailure,
    ModerationReroll,
    ModerationResult,
    ModerationSuccess,
    assemble_twitter_to_song_prompt,
    classify_text_lang,
    does_text_contain_slur,
    get_moderation_result,
    get_stanzas,
    moderate_gpt_description_prompt,
    moderate_user_inputs,
)
from suno_utils.worker.generate_song_lyrics import (
    get_prompt_from_gpt_description_prompt as get_lyrics_prompt_from_gpt_description_prompt,
)
from suno_utils.worker.loader import S3Loader
from suno_utils.worker.lyrics_client import LyricsClient
from suno_utils.worker.modal_base import get_modal_base_image
from suno_utils.worker.modal_model_configuration import DEFAULT_API_TO_MODEL_MAP, EXPERIMENTAL_INFO
from suno_utils.worker.schema import HistoryPrompt, QueueItem
from suno_utils.worker.tracing import distributed_trace, serialize_context, tracer
from suno_utils.worker.vertex_client import make_vertex_client
from suno_utils.worker.copyright_detector import CopyrightDetector
from suno_utils.worker.tag_augmentation import upsample_prompt, downsample_prompt
from suno_utils.worker.modal_model_volume import (
    MODEL_STORE_VOLUME_DIR,
    model_store_volume,
    MODEL_STORE_VOLUME_PREFIX,
    FASTTEXT_CKPT_PATH,
)

CONDUCTOR_DDOG_SERVICE = "conductor-worker"
CHATGPT_DDOG_SERVICE = "chatgpt-worker"

openai_secret = modal.Secret.from_name("openai-secret")
together_secret = modal.Secret.from_name("together-secret")

logger = logging.getLogger(__name__)
logging.basicConfig()
logger.setLevel(logging.DEBUG)

############## CHANGE THESE ##############
DEPLOYMENT_TYPE = "dev"
##########################################
DEPLOYMENT_TYPES = {"dev", "prod"}
assert DEPLOYMENT_TYPE in DEPLOYMENT_TYPES
# orchestrator talks to studio api
APP_NAME = f"orchestrator-{DEPLOYMENT_TYPE}"
assert APP_NAME.endswith(DEPLOYMENT_TYPE)

ResponseDict = dict[str, Any]

if DEPLOYMENT_TYPE == "dev":
    from suno_utils.worker.modal_model_configuration import MODEL_DEV_FNS as MODAL_FNS
elif DEPLOYMENT_TYPE == "prod":
    from suno_utils.worker.modal_model_configuration import MODEL_PROD_FNS as MODAL_FNS
else:
    err_msg = f"DEPLOYMENT_TYPE {DEPLOYMENT_TYPE} unrecognized"
    raise AssertionError(err_msg)

# For dev, we use fixed random seed; for prod, we use random seed based on time
random_seed = 42 if DEPLOYMENT_TYPE == "dev" else int((time.time() * 1000) % 100000000)
print("Random seed set to:", random_seed)
random.seed(random_seed)

# events queue for streaming events
EVENTS_QUEUE_NAME = f"events-queue-{DEPLOYMENT_TYPE}"

DD_SAMPLE_RATE = "1" if DEPLOYMENT_TYPE == "dev" else "0.0001"
# model for unit tests
TEST_MODEL_NAME = "chirp-v3-engine-i"
# models that check for copyright infringement
# we turn on it for all on Jan 23 2025
COPYRIGHT_INFRINGEMENT_MODELS = ["chirp-v3-5-tau", "chirp-v4-tau"]
# modal traffic lookup
MODAL_TRAFFIC_WORKER = "engine-chirpv2_engine_13b_special_8"
# check if the workers exist and fail early
# since these workers are used for worker inits
# if sth is wrong this fails at deploytime
COPYRIGHT_INFRINGEMENT_THRESHOLD = 0.9
BIT_ENABLE_CHECK_FOR_LYRICS_COPYRIGHT_IN_GENERATION = 2
BIT_BYPASS_LYRICS_MODERATION = 4
# looks up the H100 traffic
_ = modal.Cls.from_name(
    f"{MODAL_TRAFFIC_WORKER}_" + f"{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}_240s_H100",
    "ChirpV2Stub",
)().generate
_ = modal.Cls.from_name(
    f"history_encoder-{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}",
    "HistoryEncoderStub",
)().encode_history
_ = modal.Cls.from_name(
    f"sdxl-{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}",
    "StableDiffusion",
)().generate_image_item


def _is_base_v3_model_name(model_name):
    if model_name is None:
        return False
    v3p5_model_prefixes = ["chirp-v3p5", "chirp-v3-5"]
    return "chirp-v3" in model_name and not any(
        model_name.startswith(prefix) for prefix in v3p5_model_prefixes
    )


def _get_lyrics_length_for_model_family(model_name: str | None) -> LyricsLength:
    """Infer the desired number of stanzas for the given model."""

    if model_name and "short" in model_name:  # this is a 30 sec model
        lyrics_length = LyricsLength.SHORT
    elif _is_base_v3_model_name(model_name):
        lyrics_length = LyricsLength.STANDARD
    else:
        lyrics_length = LyricsLength.LONG
    print("for model name:", model_name, "assigning lyrics_length:", lyrics_length)
    return lyrics_length


class ChatGptWorker(S3Loader):
    """Provide nofication functionality for ChatGptStub."""

    def preload(self) -> None:
        """Preload models."""
        start_time = time.time()
        print("MOUNT_PATH:", MODEL_STORE_VOLUME_PREFIX, "FASTTEXT_CKPT_PATH:", FASTTEXT_CKPT_PATH)
        # fast text is a special loading case, it needs the full path
        ckpt_path = os.path.join(MODEL_STORE_VOLUME_PREFIX, FASTTEXT_CKPT_PATH.lstrip("/"))
        print("ckpt_path:", ckpt_path)
        chirp_v2.load_fasttext_model(ckpt_path)

        finish_time = time.time()
        print(f"Preloading took {finish_time - start_time}s")


def _make_success_response_dict(item: QueueItem, prompt_type: str, text_prompt: str) -> ResponseDict:
    return {
        "id": item.id,
        prompt_type: text_prompt,
        "moderation_result": "success",
        "ok": 1,
    }


def _make_failure_response_dict(
    item: QueueItem,
    prompt_type: str,
    text_prompt: str,
    err_msg: str,
) -> ResponseDict:
    return {
        "id": item.id,
        "type": "error",
        "moderation_result": "failure",
        prompt_type: text_prompt,
        "error_message": err_msg,
        "ok": 0,
    }


def _make_reroll_response_dict(
    item: QueueItem,
    prompt_type: str,
    text_prompt: str,
    err_msg: str,
) -> ResponseDict:
    return {
        "id": item.id,
        "type": "error",
        "moderation_result": "reroll",
        prompt_type: text_prompt,
        "error_message": err_msg,
        "ok": 0,
    }


def _make_generic_error_response_dict(
    item: QueueItem,
    prompt_type: str,
    text_prompt: str,
    err_msg: str,
) -> ResponseDict:
    return {
        "id": item.id,
        "type": "error",
        prompt_type: text_prompt,
        "error_message": err_msg,
        "ok": 0,
    }


aws_secret = modal.Secret.from_name("studio-aws")
sample_rule = json.dumps([{"sample_rate": DD_SAMPLE_RATE}])
SECRETS = [
    aws_secret,
    modal.Secret.from_name("openai-secret"),
    modal.Secret.from_name("together-secret"),
    modal.Secret.from_dict(
        {
            "DD_SITE": "datadoghq.com",
            "DD_ENV": DEPLOYMENT_TYPE,
            "DD_SERVICE": "chatgpt-worker",
            "DD_LOGS_ENABLED": "true",
            "DD_TRACE_ENABLED": "true",
            "DD_TRACE_SAMPLING_RULES": sample_rule,
        },
    ),
    modal.Secret.from_name("datadog-metrics"),
    modal.Secret.from_name("api-callback-token"),
    modal.Secret.from_name("google-application-credentials-data"),
    modal.Secret.from_name("lyrics-mod-es-creds"),
]


base_image = (
    get_modal_base_image()
    .pip_install("together==1.4.0")
    .pip_install("elasticsearch>=8.14,<9")
    .pip_install("langdetect>=1,<2")
)


image = base_image.add_local_python_source("suno_utils", copy=False)

app = modal.App(
    APP_NAME,
    image=image,
    secrets=SECRETS,
)


class GenerationModerationSub:
    """General moderation sub class for moderation of arbitrary text."""

    @modal.enter()
    def on_modal_enter(self):
        self.openai_client = OpenAI()

    @tracer.wrap()
    def _moderate_text(
        self,
        queue_item_json: str,
        key: str,
        moderation_function: Callable[[OpenAI, str], ModerationResult],
    ) -> ResponseDict:
        """Moderate arbitrary text, returning a response for ChatGptWorker.notify_finish."""
        from suno_utils.worker.schema import QueueItem

        item = QueueItem.model_validate_json(queue_item_json)
        if (text_prompt := item.metadata.get(key)) is None:
            err_msg = f"item: {item} of type {type(item)} has no key `{key}`, cannot moderate text."
            item.notify_progress(
                _make_generic_error_response_dict(
                    item,
                    key,
                    text_prompt="ERROR: PROMPT NOT FOUND",
                    err_msg=err_msg,
                ),
            )
            raise RuntimeError(err_msg)

        try:
            moderation_result = (moderation_function)(self.openai_client, text_prompt)
        except Exception as e:
            import traceback

            response_dict = {"id": item.id, "ok": 0, "type": "error", "error_message": str(e)}
            item.notify_progress(response_dict)
            traceback.print_exc()
            raise

        match moderation_result:
            case ModerationSuccess():
                response_dict = _make_success_response_dict(item, key, text_prompt)
                item.notify_progress(response_dict)
            case ModerationFailure(err_msg):
                response_dict = _make_failure_response_dict(item, key, text_prompt, err_msg)
                item.notify_progress(response_dict)
            case ModerationReroll(err_msg):
                response_dict = _make_reroll_response_dict(item, key, text_prompt, err_msg)
                item.notify_progress(response_dict)
            case unrecognized:
                err_msg = f"Received unrecognized result from moderation: {unrecognized}; this is a bug."
                response_dict = _make_generic_error_response_dict(item, key, text_prompt, err_msg)
                item.notify_progress(response_dict)
                raise RuntimeError(err_msg)

        return response_dict


@app.cls(
    cpu=1.0,
    memory=1_000,
    min_containers=1 if DEPLOYMENT_TYPE == "dev" else 10,
    timeout=240,
    scaledown_window=240,
    secrets=SECRETS,
)
@modal.concurrent(max_inputs=2)
class TextModerationSub(GenerationModerationSub):
    """Moderate arbitrary text with ChatGPT."""

    # DEPRECATED!  TK

    @modal.enter()
    def on_modal_enter(self):
        self.openai_client = OpenAI()

    @modal.method()
    def moderate_text(self, queue_item_json: str) -> ResponseDict:
        """Moderate text with ChatGPT and report result to results queue."""
        return self._moderate_text(
            queue_item_json,
            key="text",
            moderation_function=get_moderation_result,
        )


@app.cls(
    cpu=1.0,
    memory=1_000,
    min_containers=1 if DEPLOYMENT_TYPE == "dev" else 10,
    timeout=240,
    scaledown_window=240,
    secrets=SECRETS,
)
@modal.concurrent(max_inputs=2)
class TextModerationStub(GenerationModerationSub):
    """Moderate arbitrary text with ChatGPT."""

    @modal.enter()
    def on_modal_enter(self):
        self.openai_client = OpenAI()

    @modal.method()
    def moderate_text(self, queue_item_json: str) -> ResponseDict:
        """Moderate text with ChatGPT and report result to results queue."""
        return self._moderate_text(
            queue_item_json,
            key="text",
            moderation_function=get_moderation_result,
        )


@app.cls(
    cpu=1.0,
    memory=1_000,
    min_containers=1 if DEPLOYMENT_TYPE == "dev" else 10,
    timeout=240,
    scaledown_window=240,
    secrets=SECRETS,
)
@modal.concurrent(max_inputs=2)
class PromptUpsamplerStub:
    """Upsample a prompt for semantic only model (auk+) style descriptions"""

    @modal.enter()
    def on_modal_enter(self):
        self.openai_client = OpenAI()

    @modal.method()
    def upsample(self, prompt: str, is_instrumental: bool = False, check_artist_names=False) -> str:
        """Moderate text with ChatGPT and report result to results queue."""
        print("upsampling:", prompt)
        result = upsample_prompt(
            self.openai_client, prompt, is_instrumental, check_artist_names=check_artist_names
        )
        print("result:", result)
        # TODO(pat): this is rollover cruft, delete later
        if check_artist_names is False:
            return result
        else:
            if isinstance(result, str):
                return {"ok": True, "result": result}
            elif isinstance(result, ModerationFailure):
                return {"ok": False, "error_message": result.err_msg}


def _truncate_downsampled_prompt(prompt: str, max_length=100):
    if len(prompt) < max_length:
        return prompt
    truncated_tags = []
    current_length = 0
    prompt_tags = [tag.strip() for tag in prompt.split(",")]
    for tag in prompt_tags:
        # account for the length of the separator if not the first tag
        new_length = current_length + len(tag) + (0 if not truncated_tags else 2)
        if new_length <= max_length:
            truncated_tags.append(tag)
            current_length = new_length
        else:
            break
    return ", ".join(truncated_tags)


@app.cls(
    cpu=1.0,
    memory=1_000,
    min_containers=1 if DEPLOYMENT_TYPE == "dev" else 100,
    timeout=240,
    scaledown_window=240,
    secrets=SECRETS,
)
@modal.concurrent(max_inputs=2)
class PromptDownsamplerStub:
    """Downsample a prompt to extract genre tags"""

    @modal.enter()
    def on_modal_enter(self):
        self.openai_client = OpenAI()

    @modal.method()
    def downsample(self, prompt: str) -> str | None:
        """Downsample a prompt to extract genre tags."""
        return downsample_prompt(self.openai_client, prompt)

    @modal.method()
    def downsample_tags_for_item(
        self, queue_item_json: str, parent_context: Optional[Dict[str, Any]] = None
    ):
        """Downsample a prompt to extract genre tags."""
        from suno_utils.worker.schema import QueueItem

        item = QueueItem.model_validate_json(queue_item_json)
        logger.debug(queue_item_json)
        prompt = item.metadata.get("tags")
        logger.debug(prompt)
        if prompt is None or len(prompt) == 0:
            logger.debug("returning early due to empty prompt")
            return

        downsampled_prompt = downsample_prompt(self.openai_client, prompt)
        if downsampled_prompt is not None:
            cleaned_downsampled_prompt = _truncate_downsampled_prompt(downsampled_prompt)
            for id in item.all_child_clip_ids:
                payload = {
                    "id": id,
                    "type": "downsampled_tags",
                    "downsampled_tags": cleaned_downsampled_prompt,
                }
                logger.debug(f"sending payload to callback: {payload}")
                item.notify_progress(payload)


@app.cls(
    cpu=1.0,
    memory=1_000,
    min_containers=1 if DEPLOYMENT_TYPE == "dev" else 40,
    timeout=240,
    scaledown_window=240,
    secrets=SECRETS,
    volumes={MODEL_STORE_VOLUME_DIR: model_store_volume},
)
@modal.concurrent(max_inputs=2)
class PromptGenerationStub(GenerationModerationSub):
    """Generate a prompt from a GPT description prompt."""

    @modal.enter()
    def on_modal_enter(self):
        from together import Together

        self.openai_client = OpenAI()
        together_client = Together()
        self.worker = ChatGptWorker()
        self.worker.preload()

        vertex_client = make_vertex_client()

        self.lyrics_client = LyricsClient(
            self.openai_client, together_client, vertex_client, bedrock_client
        )
        self.text_lang_model = chirp_v2.text_lang_model
        if self.text_lang_model is None:
            msg = "chirp_v2.text_lang_model was not loaded successfully."
            raise RuntimeError(msg)
        print("Finished loading lang model.")

        self.events_queue = modal.Queue.from_name(EVENTS_QUEUE_NAME, create_if_missing=True)
        print("Finished init!")

    @modal.method()
    def get_prompt_from_gpt_description_prompt(self, queue_item_json: str) -> ResponseDict:
        """Given gpt_description_prompt in queue_item_json, moderate and return prompt if passing."""
        print("queue_item in get_prompt_from...", queue_item_json)
        item = QueueItem.model_validate_json(queue_item_json)
        item_without_callback = item.copy(exclude={"callback_url"}, deep=True)
        mod_response = self._moderate_text(
            item_without_callback.model_dump_json(),
            key="gpt_description_prompt",
            moderation_function=moderate_gpt_description_prompt,
        )
        if not (mod_response.get("ok") and mod_response.get("moderation_result") == "success"):
            mod_response["type"] = "generate_prompt"
            item.notify_progress(mod_response)
            return mod_response
        # else, gpt_description_prompt passed moderation
        gpt_description_prompt = item.metadata["gpt_description_prompt"]
        make_instrumental = item.metadata.get("make_instrumental", False)
        lyrics_length = _get_lyrics_length_for_model_family(item.model_name)
        lyrics_model = item.metadata.get("lyrics_model", "default")
        use_long_genre_description = False  # Turn this off for now
        print("use_long_genre_description:", use_long_genre_description)
        print("lyrics model in get prompt from gpt description prompt:", lyrics_model)
        need_to_check_generated_lyrics_for_copyright = not lyrics_model.startswith("default")
        are_lyrics_copyrighted_func = (
            CopyrightDetectorStub().are_lyrics_copyrighted.remote
            if need_to_check_generated_lyrics_for_copyright
            else None
        )

        try:
            title, prompt, genre_tags, language = get_lyrics_prompt_from_gpt_description_prompt(
                self.lyrics_client,
                self.text_lang_model,
                gpt_description_prompt,
                are_lyrics_copyrighted_func=are_lyrics_copyrighted_func,
                make_instrumental=make_instrumental,
                lyrics_length=lyrics_length,
                lyrics_model=lyrics_model,
                events_queue=self.events_queue,
                item_id=item.id,
                use_long_genre_description=use_long_genre_description,
                forced_lang=item.metadata.get("gpt_lang", None),
            )
        except ModerationError as e:
            response_dict = _make_generic_error_response_dict(
                item,
                prompt_type="gpt_description_prompt",
                text_prompt=gpt_description_prompt,
                err_msg=str(e),
            )
            response_dict["type"] = "generate_prompt"
            self.events_queue.put(
                {
                    "type": "error",
                    "data": response_dict,
                },
                partition=item.id,
                partition_ttl=60,
            )
            item.notify_progress(response_dict)
            return response_dict
        except Exception as e:
            response_dict = _make_generic_error_response_dict(
                item,
                prompt_type="gpt_description_prompt",
                text_prompt=gpt_description_prompt,
                err_msg="Unexpected Error:" + str(e),
            )
            response_dict["type"] = "generate_prompt"
            self.events_queue.put(
                {
                    "type": "error",
                    "data": response_dict,
                },
                partition=item.id,
                partition_ttl=60,
            )
            item.notify_progress(response_dict)
            return response_dict

        self.events_queue.put(
            {
                "type": "lyrics",
                "data": {
                    "title": title,
                    "prompt": prompt,
                    "genre_tags": genre_tags,
                    "lang": language,
                },
            },
            partition=item.id,
            partition_ttl=60,
        )
        response_dict = {
            "id": item.id,
            "type": "generate_prompt",
            "title": title,
            "prompt": prompt,
            "genre_tags": genre_tags,
            "ok": 1,
        }
        item.notify_progress(response_dict)
        return response_dict


@app.cls(
    cpu=1.0,
    memory=1_000,
    min_containers=1 if DEPLOYMENT_TYPE == "dev" else 100,
    timeout=240,
    scaledown_window=240,
    secrets=SECRETS,
    volumes={MODEL_STORE_VOLUME_DIR: model_store_volume},
)
@modal.concurrent(max_inputs=2)
class ChatGptStub(GenerationModerationSub):
    """Orchestrate lyrics generation and moderation."""

    @modal.enter()
    def on_modal_enter(self):
        """Set up ChatGptStub."""
        print("Start stub init.")

        from together import Together

        self.openai_client = OpenAI()
        together_client = Together()
        vertex_client = make_vertex_client()

        self.lyrics_client = LyricsClient(
            self.openai_client, together_client, vertex_client, bedrock_client
        )
        self.worker = ChatGptWorker()
        self.worker.preload()
        self.text_lang_model = chirp_v2.text_lang_model
        if self.text_lang_model is None:
            msg = "chirp_v2.text_lang_model was not loaded successfully."
            raise RuntimeError(msg)
        print("Finished loading lang model.")

        # Initialize events queue for streaming events
        self.events_queue = modal.Queue.from_name(EVENTS_QUEUE_NAME, create_if_missing=True)

        # save some modal functions -- for the main chirp worker
        self.modal_f_image_extraction = modal.Cls.from_name(
            f"image-extraction-{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}",
            "ImageExtractionStub",
        )().generate_image_description

        self.modal_f_image_extraction_from_url = modal.Cls.from_name(
            f"image-extraction-{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}",
            "ImageExtractionStub",
        )().generate_image_description_from_image_url

        self.modal_f_search_tavily = modal.Cls.from_name(
            f"brave-search-{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}",
            "SearchWorker",
        )().search_with_tavily

        self.modal_compose_search_query = modal.Cls.from_name(
            f"brave-search-{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}",
            "SearchWorker",
        )().compose_search_query

        # Add image generation modal functions
        self.modal_f_image_generator = modal.Cls.from_name(
            f"sdxl-{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}",
            "StableDiffusion",
        )().generate_image_item

        self.modal_f_pro_image_generator = modal.Cls.from_name(
            f"flux-{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}",
            "StableDiffusion",
        )().generate_image_item

        self.modal_f_lyrics_mod = modal.Cls.from_name(
            f"orchestrator-{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}", "CopyrightDetectorStub"
        )().are_lyrics_copyrighted

        print("Finished init!")

    def _predict_lyrics_language(self, lyrics: str) -> str:
        """Moderate lyrics with ChatGPT and report result to results queue."""
        # Could turn it into an API function in the future if needed.
        lyrics_chunks = [chunk.replace("\n", " ").strip() for chunk in lyrics.split("\n\n")]
        # keep only the meaningful ones
        lyrics_chunks = [chunk for chunk in lyrics_chunks if len(chunk) >= 5]
        language_chunks = [classify_text_lang(self.text_lang_model, chunk) for chunk in lyrics_chunks]

        # Count occurrences of each language using Counter
        language_counts = Counter(lang for lang in language_chunks if lang is not None)

        # Find the most common language(s)
        if language_counts:
            most_common = language_counts.most_common(1)
            if most_common:
                most_common_language, count = most_common[0]
                # Check if there are other languages with the same count
                if count / len(language_counts) > 0.5:  # Multiple languages with the same highest count
                    return most_common_language
        return ""  # No languages found is empty string

    def _get_text_from_all_images(
        self, item: QueueItem, image_to_song_s3_ids: list[str]
    ) -> str | ImageModerationFailure | ImageExtractionError:
        """Run image extraction modal runner on all images, save first image as clip cover image, and return descriptions string."""
        print("running image extraction")
        # run image extraction modal runner on each image, and notify failed clip if error
        try:
            images_description = self.modal_f_image_extraction.remote(
                QueueItem(
                    id=item.id,
                    metadata={
                        "image_to_song_s3_ids": image_to_song_s3_ids,
                        "user_context": item.metadata.get("gpt_description_prompt", ""),
                    },
                ).model_dump_json()
            )
        # network errors in image extraction pipeline
        except Exception as e:
            # modal worker tried 3 times
            return ImageExtractionError(e)

        # return text description for ChatGPTStub's lyric generation
        return images_description

    def _get_text_from_image_url(
        self, item: QueueItem
    ) -> str | ImageModerationFailure | ImageExtractionError:
        """Get text from image url."""
        try:
            image_description = self.modal_f_image_extraction_from_url.remote(item.model_dump_json())
        except Exception as e:
            return ImageExtractionError(e)
        return image_description

    def _save_user_cover_image(self, item: QueueItem, s3_id: str):
        """Save the  image to the clip cover image."""

        if item.all_child_clip_ids:
            # TODO: do we need a different image for each clip for stems?
            for clip_id in item.all_child_clip_ids:
                clip_queue_item = QueueItem(id=clip_id, metadata={"image_s3_id": s3_id})

                # save different size copies to s3 for display and video
                self.worker.copy_and_upload_image(clip_queue_item)

                # send progress to finish-clip using callback
                print("sending image progress now")
                item.notify_progress(
                    {
                        "id": clip_id,
                        "type": "image",
                        "image_id": f"image_{clip_id}",  # TODO: should this be s3_id?
                    },
                )

        pass

    def _maybe_process_image_to_song(self, item: QueueItem) -> None:
        """If an image_to_song gen, extract text and generate GPT description prompt for item.  Mutates argument."""
        image_to_song_s3_ids = item.metadata.get("image_to_song_s3_ids")
        if not image_to_song_s3_ids:
            return
        tick = time.time()

        # call OpenAI Vision to get description of image
        description = self._get_text_from_all_images(item, image_to_song_s3_ids)

        # catch image related failures and notify user
        if isinstance(description, ImageModerationFailure):
            err_msg = "We couldn't generate a song for this image. Please try another."
            item.notify_progress(
                {
                    "id": item.id,
                    "type": "error",
                    "error_type": "image_moderation_failure",
                    "error_message": err_msg,
                },
            )
            raise ModerationError(err_msg)
        elif isinstance(description, ImageExtractionError):
            err_msg = f"Image extraction failed: {description}"
            item.notify_progress(
                {
                    "id": item.id,
                    "type": "error",
                    "error_type": "image_extraction_failure",
                    "error_message": err_msg,
                },
            )
            raise ImageExtractionError(err_msg)

        # save first image as display image for each clip
        first_image_s3_id = image_to_song_s3_ids[0]
        self._save_user_cover_image(item, first_image_s3_id)

        # combine image description with user context to make lyric prompt
        item.metadata["gpt_description_prompt"] = (
            (item.metadata.get("gpt_description_prompt", "") or "") + "\n" + description
        )
        tock = time.time()
        print(f"Image to song EXTRACTION took {tock - tick} seconds")

    def _maybe_process_video_to_song(self, item: QueueItem) -> None:
        """If video to song, we pass in the video description as part of the prompt.  Mutates argument."""
        if video_to_song_description := item.metadata.get("video_to_song_description"):
            item.metadata["gpt_description_prompt"] = (
                (item.metadata.get("gpt_description_prompt", "") or "")
                + "\n"
                + video_to_song_description
            )

    def _maybe_process_twitter_to_song(self, item: QueueItem) -> None:
        """If twitter to song, we pass in the twitter content as part of the prompt.  Mutates argument."""
        image_description, search_results = None, None
        if twitter_to_song_content := item.metadata.get("twitter_to_song_content"):
            item.metadata["gpt_description_prompt"] = (
                (item.metadata.get("gpt_description_prompt", "") or "") + "\n" + twitter_to_song_content
            )
            item.metadata["is_twitter_to_song"] = True
            if item.metadata.get("twitter_image_url"):
                image_description = self._get_text_from_image_url(item)
                if isinstance(image_description, ImageModerationFailure):
                    err_msg = "We couldn't generate a song for this image. Please try another."
                    item.notify_progress(
                        {
                            "id": item.id,
                            "type": "error",
                            "error_type": "image_moderation_failure",
                            "error_message": err_msg,
                        },
                    )
                    raise ModerationError(err_msg)
                elif isinstance(image_description, ImageExtractionError):
                    err_msg = f"Image extraction failed: {image_description}"
                    item.notify_progress(
                        {
                            "id": item.id,
                            "type": "error",
                            "error_type": "image_extraction_failure",
                            "error_message": err_msg,
                        },
                    )
                    raise ImageExtractionError(err_msg)
                item.metadata["gpt_description_prompt"] = (
                    (item.metadata.get("gpt_description_prompt", "") or "")
                    + "\n Tweet Image Description: "
                    + image_description
                )
            try:
                search_query = self.modal_compose_search_query.remote(twitter_to_song_content)
                search_results = self.modal_f_search_tavily.remote(search_query, max_results=3, days=7)
            except Exception as e:
                print(f"[Maybe process twitter to song] Error search online: {e}")
                search_results = None
            item.metadata["gpt_description_prompt"] = assemble_twitter_to_song_prompt(
                item.metadata.get("gpt_description_prompt", ""),
                twitter_to_song_content,
                image_description,
                search_results,
            )

    def _start_early_image_generation(self, early_item, title, tags):
        """Helper function to start image generation, handling backlog logic consistently.

        Does not return anything.
        """
        # Only start early image generation if we have BOTH title AND tags
        if not title or not tags:
            return

        # This should only run if it's not an image-to-song (where we already have an image)
        if early_item.is_image_to_song:
            return

        print(f"Starting early image generation with title '{title}' and tags '{tags}'")

        # Mark that we've already started image generation to avoid duplicate runs
        early_item.metadata["image_generation_started"] = True

        # Queue up image generation task
        if isinstance(early_item.model_name, str) and (
            "auk" in early_item.model_name
            or "bluejay" in early_item.model_name
            or "v4" in early_item.model_name
        ):
            try:
                n_backlog = self.modal_f_pro_image_generator.get_current_stats().backlog
                if n_backlog < 25:  # reduce the backlog to 25
                    _ = self.modal_f_pro_image_generator.spawn(
                        early_item.model_dump_json(),
                        parent_context=serialize_context(),
                    )
                else:
                    _ = self.modal_f_image_generator.spawn(
                        early_item.model_dump_json(),
                        parent_context=serialize_context(),
                    )
            except Exception as e:
                print(f"Error starting early image generation: {e}")
        else:
            try:
                _ = self.modal_f_image_generator.spawn(
                    early_item.model_dump_json(), parent_context=serialize_context()
                )
            except Exception as e:
                print(f"Error starting early image generation: {e}")

    @modal.method()
    @distributed_trace("moderate_and_generate", CHATGPT_DDOG_SERVICE, env_name=DEPLOYMENT_TYPE)
    def moderate_and_generate(self, queue_item_json: str, use_openai_mod_for_custom_lyrics=True):
        """Moderate and generate.

        This worker will go through all the moderation steps.
        If successful, will call the orchestrate_and_generate worker.
        """
        item = QueueItem(**json.loads(queue_item_json))
        override_fields = item.metadata.get("override_fields", [])
        print(
            f"Moderator: {item.metadata.get('ids')} is free: {item.is_free_generation}",
        )
        print("in moderate_and_generate with item:", item)
        use_long_genre_description = False
        print(
            "audio model name:",
            item.model_name,
            "use_long_genre_description:",
            use_long_genre_description,
        )
        # generate lyrics and tags
        lyrics_length = _get_lyrics_length_for_model_family(item.model_name)

        try:
            self._maybe_process_image_to_song(item)
            self._maybe_process_video_to_song(item)
            self._maybe_process_twitter_to_song(item)
        except:
            print(f"Got error processing image or video for clip {item.id}. Bailing on generation.")
            return

        using_custom_title = False
        is_twitter_to_song = item.metadata.get("is_twitter_to_song", False)
        if user_prompt := item.metadata.get("gpt_description_prompt"):
            try:
                lyrics_model = item.metadata.get("lyrics_model", "default")
                print("lyrics_model:", lyrics_model)
                are_lyrics_copyrighted_func = (
                    CopyrightDetectorStub().are_lyrics_copyrighted.remote
                    if not lyrics_model.startswith("default")
                    else None
                )

                # Define early title/tags callback function to start image generation early
                title_tags_early_callback = None

                # Only enable early callback if requested in metadata
                if item.metadata.get("extra", {}).get("early_callback", False):

                    def title_tags_early_callback(title: str, tags: str) -> None:
                        if not title:
                            return

                        # Update item with early title/tags
                        early_item = item.model_copy(deep=True)
                        early_item.title = title

                        # Process tags if available
                        if tags:
                            early_item.metadata["tags"] = tags

                        # Notify progress with early title and tags
                        if early_item.all_child_clip_ids:  # Request level item
                            for clip_id in early_item.all_child_clip_ids:
                                notify_data = {
                                    "id": clip_id,
                                    "title": title,
                                    "type": "early_title_tags",
                                }
                                if tags:
                                    notify_data["tags"] = tags

                                early_item.notify_progress(notify_data, blocking=False)
                        else:  # Clip level item
                            notify_data = {
                                "id": early_item.id,
                                "title": title,
                                "type": "early_title_tags",
                            }
                            if tags:
                                notify_data["tags"] = tags

                            early_item.notify_progress(notify_data, blocking=False)

                        # Start image generation if we have both title and tags (using helper)
                        self._start_early_image_generation(early_item, title, tags)

                use_moderation_for_gpt_description_prompt = False if is_twitter_to_song else True
                title, lyrics, genre_tags, language = get_lyrics_prompt_from_gpt_description_prompt(
                    self.lyrics_client,
                    chirp_v2.text_lang_model,
                    user_prompt,
                    use_moderation_for_gpt_description_prompt=use_moderation_for_gpt_description_prompt,
                    check_artist_names=item.metadata.get("check_artist_names", True),
                    make_instrumental=item.metadata.get("make_instrumental", False),
                    lyrics_length=lyrics_length,
                    is_image_or_video_to_song=item.is_image_to_song or item.is_video_to_song,
                    is_twitter_to_song=is_twitter_to_song,
                    lyrics_model=item.metadata.get("lyrics_model", "default"),
                    events_queue=self.events_queue,
                    item_id=item.id,
                    are_lyrics_copyrighted_func=are_lyrics_copyrighted_func,
                    use_long_genre_description=use_long_genre_description,
                    title_tags_callback=title_tags_early_callback,
                    forced_lang=item.metadata.get("gpt_lang", None),
                )

            except ModerationError as mod_error:
                import traceback

                # Write error to request-level event queue
                self.events_queue.put(
                    {
                        "type": "error",
                        "data": {
                            "error_type": "moderation_failure",
                            "error_message": str(mod_error),
                        },
                    },
                    partition=item.id,  # Request-level partition
                    partition_ttl=60,
                )

                # Write error to clip-level event queues
                for clip_id in item.all_child_clip_ids:
                    item.notify_progress(
                        {
                            "id": clip_id,
                            "type": "error",
                            "error_type": "moderation_failure",
                            "error_message": str(mod_error),
                        },
                    )
                    self.events_queue.put(
                        {
                            "type": "error",
                            "data": {
                                "error_type": "moderation_failure",
                                "error_message": str(mod_error),
                            },
                        },
                        partition=clip_id,
                        partition_ttl=60,
                    )
                traceback.print_exc()
                return
            except Exception as e:
                import traceback

                # Write error to request-level event queue
                self.events_queue.put(
                    {
                        "type": "error",
                        "data": {
                            "error_type": "moderation_failure",
                            "error_message": str(e),
                        },
                    },
                    partition=item.id,  # Request-level partition
                    partition_ttl=60,
                )

                # Write error to clip-level event queues
                for clip_id in item.all_child_clip_ids:
                    item.notify_progress(
                        {
                            "id": clip_id,
                            "type": "error",
                            "error_type": "moderation_failure",
                            "error_message": "Generic openAI error",
                        },
                    )
                    self.events_queue.put(
                        {
                            "type": "error",
                            "data": {
                                "error_type": "moderation_failure",
                                "error_message": str(e),
                            },
                        },
                        partition=clip_id,
                        partition_ttl=60,
                    )
                traceback.print_exc()
                return

            if item.title is None or item.title == "":
                # Use generated title if available, otherwise fall back to title_fallback from extra if provided
                if title:
                    item.title = title
                elif item.metadata.get("extra", {}).get("title_fallback"):
                    item.title = item.metadata["extra"]["title_fallback"]
                else:
                    item.title = title  # This will be None or empty string
            else:
                using_custom_title = True
            if item.prompt_text is None or item.prompt_text == "" or "prompt" not in override_fields:
                item.prompt_text = lyrics
            if (
                item.metadata.get("tags") is None
                or item.metadata.get("tags") == ""
                or "tags" not in override_fields
            ):
                item.metadata["tags"] = ", ".join(genre_tags)

            item.metadata["lang"] = language
            logger.info(f"generated {item.metadata['tags']} {lyrics}")

        if (
            user_prompt is None or len(override_fields) > 0 or using_custom_title
        ):  # there's no gpt_description prompt, so we're dealing with custom lyrics.
            # if using override fields or custom title, we need to moderate here
            # IN CASE OF openAI outage, moderation could be down, flag this to False
            # TDDO: backups and fallbacks on this logic
            moderate_lyrics = True

            tags = item.metadata.get("tags") or ""
            language = self._predict_lyrics_language(item.prompt_text or "")
            item.metadata["lang"] = language
            if moderate_lyrics:
                if use_openai_mod_for_custom_lyrics:
                    moderation_result = moderate_user_inputs(
                        self.openai_client, item.prompt_text or "", tags, item.title or ""
                    )
                    logger.debug(moderation_result)
                else:
                    text_to_be_moderated = "\n".join([item.title or "", item.prompt_text or "", tags])

                    moderation_result: ModerationResult = (
                        ModerationSuccess()
                        if not does_text_contain_slur(text_to_be_moderated)
                        else ModerationFailure("manually flagged")
                    )
                # In all cases where lyrics are provided, run lyrics mod v3
                try:
                    print(f"Checking for copyrighted lyrics in Elasticsearch!: {item.prompt_text}")
                    lyrics_are_copyrighted = (
                        self.modal_f_lyrics_mod.spawn(item.prompt_text).get(
                            timeout=30  # normally takes ~400ms but coldstarts on the copyright worker can take 15-30 seconds.
                        )
                        if item.prompt_text
                        else False
                    )
                    if lyrics_are_copyrighted:
                        print("Copyrighted lyrics were found in Elasticsearch.")
                        moderation_result = ModerationFailure("Lyrics contained copyrighted material")
                except Exception as e:
                    print(f"Error checking lyrics copyright: {e}")
                    import traceback

                    traceback.print_exc()
                    lyrics_are_copyrighted = False

                if not isinstance(moderation_result, ModerationSuccess):
                    # Write error to request-level event queue
                    self.events_queue.put(
                        {
                            "type": "error",
                            "data": {
                                "error_type": "moderation_failure",
                                "error_message": moderation_result.err_msg,
                            },
                        },
                        partition=item.id,  # Request-level partition
                        partition_ttl=60,
                    )

                    # Write error to clip-level
                    print("Broadcasting moderation failures")
                    for clip_id in item.all_child_clip_ids:
                        item.notify_progress(
                            {
                                "id": clip_id,
                                "type": "error",
                                "error_type": "moderation_failure",
                                "error_message": moderation_result.err_msg,
                            },
                        )
                        # Also add to clip-level events queue
                        self.events_queue.put(
                            {
                                "type": "error",
                                "data": {
                                    "error_type": "moderation_failure",
                                    "error_message": moderation_result.err_msg,
                                },
                            },
                            partition=clip_id,
                            partition_ttl=60,
                        )
                    return
        if item.metadata.get("make_instrumental", False):
            # make sure we set the langeuge to None if definitely instrumental
            item.metadata["lang"] = None

        # watermarking
        if item.metadata.get("is_copycat") or item.metadata.get("is_audio_watermarked"):
            item.prompt_text = f"{SUNO_AD} {item.prompt_text} {SUNO_AD}"

        # send the lyrics to the latency-sensitiveevents queue
        if item.metadata.get("gpt_description_prompt"):
            event_data = {
                "title": item.title,
                "prompt": item.prompt_text,
                "genre_tags": item.metadata["tags"],
                "lang": item.metadata["lang"],
            }
            self.events_queue.put(
                {
                    "type": "lyrics",
                    "data": event_data,
                },
                partition=item.id,
                partition_ttl=60,
            )
            if user_prompt := item.metadata.get("gpt_description_prompt"):
                if item.all_child_clip_ids:  # this is a request level item
                    for clip_id in item.all_child_clip_ids:
                        self.events_queue.put(
                            {
                                "type": "lyrics",
                                "data": event_data,
                            },
                            partition=clip_id,
                            partition_ttl=60,
                        )

        # at this stage, we can pass this onto orchestrator successfully
        _ = ConductorStub.redirect_and_generate.spawn(
            item.model_dump_json(), parent_context=serialize_context()
        )
        # we notify the update after we spawn to save 1 sec TODO(Chi): revisit if this is necessary when we use TPE for notify_progress
        if user_prompt := item.metadata.get("gpt_description_prompt"):
            # this notification needs to be done at clip level, since this is per clip lyrics?
            # TODO: only for now -- in the future, we should always notify progress on the clip ids
            if item.all_child_clip_ids:  # this is a request level item
                for clip_id in item.all_child_clip_ids:
                    item.notify_progress(
                        {
                            "id": clip_id,
                            "title": item.title,
                            "type": "lyrics",
                            "text": item.prompt_text,
                            "tags": item.metadata["tags"],
                            "lang": item.metadata["lang"],
                        },
                    )
            else:  # this is a clip level item
                item.notify_progress(
                    {
                        "id": item.id,
                        "title": item.title,
                        "type": "lyrics",
                        "text": item.prompt_text,
                        "tags": item.metadata["tags"],
                        "lang": item.metadata["lang"],
                    },
                )
        return item.model_dump_json()

    @modal.method()
    def generate_lyrics_and_genre_from_gpt_instructions(self, queue_item_json: str):
        #  I think this is mostly twitter [?]  PON
        item = QueueItem(**json.loads(queue_item_json))

        # Check that we have exactly one of tweet_context or gpt_description_prompt
        tweet_context = item.metadata.get("tweet_context")
        gpt_description_prompt = item.metadata.get("gpt_description_prompt")

        if bool(tweet_context) == bool(gpt_description_prompt):
            raise ValueError(
                "Must have exactly one of tweet_context or gpt_description_prompt in metadata"
            )

        # Determine if this is a twitter to song request
        is_twitter_to_song = bool(tweet_context)
        user_prompt = tweet_context if is_twitter_to_song else gpt_description_prompt

        if user_prompt:
            try:
                title, lyrics, genre_tags, language = get_lyrics_prompt_from_gpt_description_prompt(
                    self.lyrics_client,
                    chirp_v2.text_lang_model,
                    user_prompt,
                    use_moderation_for_gpt_description_prompt=False,
                    check_artist_names=item.metadata.get("check_artist_names", True),
                    make_instrumental=item.metadata.get("make_instrumental", False),
                    lyrics_length=LyricsLength.STANDARD,
                    is_image_or_video_to_song=item.is_image_to_song or item.is_video_to_song,
                    is_twitter_to_song=is_twitter_to_song,
                    lyrics_model=item.metadata.get("lyrics_model", "default"),
                    events_queue=self.events_queue,
                    item_id=item.id,
                    are_lyrics_copyrighted_func=None,
                    use_long_genre_description=True,
                    forced_lang=item.metadata.get("gpt_lang", None),
                )
                return {
                    "title": title,
                    "lyrics": lyrics,
                    "genre_tags": genre_tags,
                    "language": language,
                }
            except Exception as e:
                return {
                    "error": str(e),
                }
        return {
            "error": "No gpt_description_prompt found",
        }


@app.cls(
    cpu=1.0,
    memory=1_000,
    min_containers=1 if DEPLOYMENT_TYPE == "dev" else 50,
    timeout=240,
    scaledown_window=240,
    secrets=SECRETS,
    volumes={MODEL_STORE_VOLUME_DIR: model_store_volume},
)
@modal.concurrent(max_inputs=4)
class CopyrightDetectorStub:
    """General moderation sub class for moderation of arbitrary text."""

    @modal.enter()
    def on_modal_enter(self):
        self.copyright_detector = CopyrightDetector(
            os.environ["LYRICS_MOD_ES_URL"], os.environ["LYRICS_MOD_ES_KEY"], DEPLOYMENT_TYPE
        )

    @modal.method()
    def are_lyrics_copyrighted(self, lyrics: str, strict: bool = False) -> bool:
        """Determine whether lyrics likely infringe on copyright"""
        print(f"Checking copyright:\n----------\n{lyrics}\n----------")
        result = self.copyright_detector.are_lyrics_copyrighted(lyrics, strict)
        print("copyright result:", result)
        return result


##########################################
# Setup the conductor stub
@app.cls(
    cpu=1.0,
    memory=1_000,
    secrets=SECRETS,
    min_containers=2 if DEPLOYMENT_TYPE == "dev" else 100,
    timeout=240,
    retries=modal.Retries(
        max_retries=2,
        backoff_coefficient=2.0,
        initial_delay=5.0,
    ),
    scaledown_window=240,
)
@modal.concurrent(max_inputs=2)
class ConductorStub(S3Loader):
    """Expose ConductorStub worker as a modal app

    Conductor is responsible for distributing queue items to the appropriate chirp worker.
    It is also responsible for controlling the traffic split for the experimental models.
    Or conducting A/B testing for the hyperparameters.
    Think of the greats: Karajan, Bernstein, Abbado, Haitink, Jansons.
    """

    @modal.enter()
    def on_modal_enter(self):
        """Set up ChatGptStub."""
        # looks up the H100 traffic
        self.modal_f_chirp_main_H100 = modal.Cls.from_name(
            f"{MODAL_TRAFFIC_WORKER}_" + f"{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}_240s_H100",
            "ChirpV2Stub",
        )().generate
        self.modal_f_history_loader = modal.Cls.from_name(
            f"history_encoder-{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}",
            "HistoryEncoderStub",
        )().encode_history
        self.modal_f_image_generator = modal.Cls.from_name(
            f"sdxl-{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}",
            "StableDiffusion",
        )().generate_image_item
        self.modal_f_pro_image_generator = modal.Cls.from_name(
            f"flux-{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}",
            "StableDiffusion",
        )().generate_image_item
        self.modal_f_downsampler = modal.Cls.from_name(
            f"orchestrator-{'dev' if DEPLOYMENT_TYPE == 'dev' else 'prod'}", "PromptDownsamplerStub"
        )().downsample_tags_for_item
        # key is a tuple of worker name and function name
        self.modal_chirp_worker_lookup_cache = {}
        # events queue for streaming events
        self.events_queue = modal.Queue.from_name(EVENTS_QUEUE_NAME, create_if_missing=True)

    def _experimental_model_mixer(self, queue_item: QueueItem) -> QueueItem:
        """Mix the queue request under the hood. For experimental purpose only."""
        temp_queue_item = queue_item.model_copy(deep=True)
        # TODO: probably need some flag to indicate if this user enroll in abtesting or not
        # only a dedicated exp model name will trigger this
        # note that this is a totally fake model, only for traffic split purpose
        # this is experiment split at clip level
        stuidio_api_to_experiment_map = EXPERIMENTAL_INFO["api_model_to_experiment_map"]
        # for bots, do sth special :)
        # now that things are faster we can spare a bit of extra traffic into them
        # but not experiments since they don't label
        if temp_queue_item.is_bot_generation:
            print(f"Warning: bot generation detected. Item ids: {temp_queue_item.all_child_clip_ids}")
            stuidio_api_to_experiment_map = EXPERIMENTAL_INFO["api_model_to_bot_model_map"]
        # AUK infill is a special case -- we will do the mapping here
        if temp_queue_item.model_name == "chirp-auk" and temp_queue_item.is_infill:
            print(f"AUK infill detected. Item ids: {temp_queue_item.all_child_clip_ids}")
            temp_queue_item.model_name = "chirp-auk-infill"
        # Somehow I feel ppl are using the model directly to pass bots is that true?
        if DEPLOYMENT_TYPE == "prod":
            # prod should only have limited models to access
            is_invalid_model_name = (temp_queue_item.model_name not in DEFAULT_API_TO_MODEL_MAP) and (
                temp_queue_item.model_name not in ["chirp-v4-h-api", "chirp-v2-engine-msft-60s"]
            )
            # these are 30b features but the input model name is wrong
            special_30b_models = ["chirp-v3-5-tau", "chirp-v4-tau", "chirp-auk-infill"]
            special_task_models = special_30b_models + [
                "chirp-auk",
                "chirp-auk-o",
                "chirp-bluejay",
                "chirp-bluejay-o",
            ]
            is_unexpected_model_name = (temp_queue_item.model_name not in special_task_models) and (
                temp_queue_item.is_infill
                or temp_queue_item.is_cover_condition
                or temp_queue_item.is_artist_condition
            )
            is_unexpected_model_name_task = (temp_queue_item.model_name not in special_task_models) and (
                temp_queue_item.metadata.get("task") == "infill"
                or temp_queue_item.metadata.get("task") == "cover"
                or temp_queue_item.metadata.get("task") == "artist_consistency"
            )
            is_unexpected_30b_model_in_prod = (
                DEPLOYMENT_TYPE == "prod"
                and (temp_queue_item.model_name in special_30b_models)
                and not (
                    temp_queue_item.is_infill
                    or temp_queue_item.is_cover_condition
                    or temp_queue_item.is_artist_condition
                    # TODO: add extend
                )
            )
            if (
                is_invalid_model_name
                or is_unexpected_model_name
                or is_unexpected_model_name_task
                or is_unexpected_30b_model_in_prod
            ):
                # We will overwrite and give you something from bot model
                if is_invalid_model_name:  # this is more like a bot behavoir
                    print(
                        f"Warning {temp_queue_item.id}: invalid model name in prod: {temp_queue_item.model_name}"
                    )
                    temp_queue_item.model_name = "chirp-v3-5-b"
                elif is_unexpected_model_name:  # this is more like a suno bug
                    print(
                        f"Warning {temp_queue_item.id}: unexpected model name given task: {temp_queue_item.model_name}"
                    )
                    temp_queue_item.model_name = "chirp-v3-5-b"
                elif is_unexpected_model_name_task:
                    print(
                        f"Warning {temp_queue_item.id}: unexpected 30b task in prod: {temp_queue_item.model_name}"
                    )
                    temp_queue_item.model_name = "chirp-v3-5-b"
                elif is_unexpected_30b_model_in_prod:
                    print(
                        f"Warning {temp_queue_item.id}: unexpected 30b model access in prod: {temp_queue_item.model_name}"
                    )
                    # TODO: remove this if once we fix the IOS model name issue
                    if temp_queue_item.metadata.get("source") != "ios":
                        temp_queue_item.model_name = "chirp-v3-5-b"
                # reroute the traffic to bot traffic split
                stuidio_api_to_experiment_map = EXPERIMENTAL_INFO["api_model_to_bot_model_map"]

                # TODO: remove this once we fix the IOS model name issue
                # Cause IOS is bugged rollout on Feb 26, 2025
                if is_unexpected_30b_model_in_prod and temp_queue_item.metadata.get("source") == "ios":
                    if temp_queue_item.model_name == "chirp-v4-tau":
                        temp_queue_item.model_name = "chirp-v4"
                    elif temp_queue_item.model_name == "chirp-v3-5-tau":
                        temp_queue_item.model_name = "chirp-v3-5"
                    stuidio_api_to_experiment_map = EXPERIMENTAL_INFO["api_model_to_experiment_map"]
        # TODO: remove this once we move everything to request level queueItem
        if not temp_queue_item.all_child_clip_ids:
            print(f"Real studio API calls should contain ids!? {queue_item}")
            # This is probably not real API requests but ppl hacking our API
            # In this case, it will default to v3.0 launch :)
            experiment_frac_to_model_name = {
                1: "chirp-v3-engine-i",
            }
            # if API -- won't be in the map, will be passing through
            if temp_queue_item.model_name in stuidio_api_to_experiment_map:
                random_value = random.random()
                for experiment_model_split_frac, model_name in experiment_frac_to_model_name.items():
                    if random_value <= experiment_model_split_frac:
                        temp_queue_item.model_name = model_name
                        logger.debug(f"switched model to {model_name}: {random_value:.3f}")
                        # log this info in metadata
                        temp_queue_item.metadata["experiment"] = model_name
                        temp_queue_item.metadata["experiment_version"] = EXPERIMENTAL_INFO[
                            "experiment_version"
                        ]
                        break

            return temp_queue_item
        # let's do the experiment at request level! This needs to be done a bit more careful
        else:
            # we do not split exp for v2, for instance
            if (
                original_temp_queue_item_model_name := temp_queue_item.model_name
            ) in stuidio_api_to_experiment_map:
                experiment_frac_to_model_names = stuidio_api_to_experiment_map[
                    original_temp_queue_item_model_name
                ]
                # need experiment traffic split
                if len(temp_queue_item.ids or temp_queue_item.multi_ids) != 2:
                    raise ValueError("This is not a valid experiment request")
                random_value = random.random()
                # overwrite 30b experiment temperorily based on task
                # we will leave infill out for auk ab test now
                # if original_temp_queue_item_model_name == "chirp-v4-tau" and (temp_queue_item.is_infill):
                #     random_value = 1.0
                for experiment_model_split_frac, model_names in experiment_frac_to_model_names.items():
                    if random_value <= experiment_model_split_frac:
                        # note that the queue item model name is modified now!
                        temp_queue_item.model_name = model_names[0]  # set a place holder for now
                        # very important that we shuffle the model names here
                        random.shuffle(model_names)
                        temp_queue_item.metadata["experiment"] = model_names
                        temp_queue_item.metadata["experiment_version"] = EXPERIMENTAL_INFO[
                            "experiment_version"
                        ]
                        logger.debug(
                            f"switched models to {temp_queue_item.metadata['experiment']}: {random_value:.3f}"
                        )
                        break
                # For inference experiments
                random_value = random.random()
                inference_experiment = EXPERIMENTAL_INFO["inference_experiment"]
                meets_experimental_condition = (
                    original_temp_queue_item_model_name in inference_experiment
                    and len(temp_queue_item.metadata.get("experiment", [])) == 2  # two model names
                    and len(set(temp_queue_item.metadata.get("experiment", []))) == 1  # but same model
                    and (
                        temp_queue_item.metadata.get("control_sliders", None) is None
                    )  # will not stack slider exps
                )
                experiment_config_split_frac_map = {}
                if meets_experimental_condition:
                    exepriment_model_name = temp_queue_item.metadata["experiment"][0]
                    experiment_config_split_frac_map = inference_experiment[
                        original_temp_queue_item_model_name
                    ].get(exepriment_model_name, {})
                for (
                    experiment_config_split_frac,
                    (param_experiment_name, model_inference_configs),
                ) in experiment_config_split_frac_map.items():
                    if (
                        meets_experimental_condition
                        and experiment_config_split_frac > 0
                        and random_value < experiment_config_split_frac
                    ):
                        # each id will have it's own model configs
                        random.shuffle(model_inference_configs)
                        # task specific experiment filters -- only freedom for diff infill
                        if "freedom" in param_experiment_name and (not temp_queue_item.is_diff_infill):
                            break
                        # rename the exp -- note how we rename it to param_experiment, for logging
                        temp_queue_item.metadata["param_experiment"] = param_experiment_name
                        temp_queue_item.metadata["model_config"] = model_inference_configs
                        logger.debug(
                            f"{temp_queue_item.id}: Switched inference config to {temp_queue_item.metadata['model_config']}: {random_value:.3f}"
                        )
                        break
                meet_mask_control_slider_experimental_condition = (
                    original_temp_queue_item_model_name in inference_experiment
                    and len(temp_queue_item.metadata.get("experiment", [])) == 2  # two model names
                    and len(set(temp_queue_item.metadata.get("experiment", []))) == 1  # but same model
                    and (
                        temp_queue_item.metadata.get("control_sliders", None) is not None
                    )  # will not stack slider exps
                )
                # 10% of control slider mask
                control_slider_experiment_frac = 0.1
                if (
                    meet_mask_control_slider_experimental_condition
                    and random_value < control_slider_experiment_frac
                ):
                    model_inference_configs = [{"masked": True}, {}]
                    random.shuffle(model_inference_configs)
                    temp_queue_item.metadata["model_config"] = model_inference_configs
                    temp_queue_item.metadata["param_experiment"] = "mask_control_slider"
                    logger.debug(
                        f"{temp_queue_item.id}: Switched to mask_control_slider: {random_value:.3f}"
                    )

            return temp_queue_item

    def _notify_queue_status_with_epxerimental_info(self, item: QueueItem) -> None:
        """Send a queue status notification with the experimental info."""
        progress_info = {
            "id": item.id,
            "type": "queued",
        }
        # log the experimental model status
        if experiment_model := item.metadata.get("experiment"):
            progress_info["experiment"] = experiment_model
            progress_info["experiment_version"] = item.metadata.get("experiment_version", "")
            progress_info["model_name"] = item.model_name if item.model_name is not None else ""
        # log the experimental param status
        if epxeriment_request_param := item.metadata.get("param_experiment"):
            progress_info["param_experiment"] = epxeriment_request_param
        if language := item.metadata.get("lang"):
            progress_info["lang"] = language
        item.notify_progress(progress_info)
        # also notify the child clip ids, especially for stems
        for child_id in item.all_child_clip_ids:
            child_item = item.model_copy(deep=True, update={"id": child_id, "ids": None})
            child_item.metadata["multi_ids"] = []
            progress_info["id"] = child_id
            child_item.notify_progress(progress_info)

    def _get_gpt_worker(self, item: QueueItem) -> modal.Function:
        if item.model_name is None:
            raise ValueError("Model name is required to get the GPT worker.")
        stub_name, stub_function = MODAL_FNS[item.model_name]
        # only for the prod 3p5 engine
        if "30b" in stub_name:
            # v4 is on H100 only
            stub_name_worker = f"{stub_name}_H200"
        elif (
            item.model_name.startswith("chirp-v4-up")
            or item.model_name.startswith("chirp-seeds")
            or item.model_name.startswith("chirp-ahi")
        ):
            # upsample model don't have GPU types yet
            stub_name_worker = f"{stub_name}"
        else:
            # we have enough H100s so most other jobs are H100s now
            stub_name_worker = f"{stub_name}_H100"
        # we don't need to lookup it again if we have already done it
        if (stub_name_worker, stub_function) in self.modal_chirp_worker_lookup_cache:
            return self.modal_chirp_worker_lookup_cache[(stub_name_worker, stub_function)]
        else:
            try:
                cls_name = stub_function.split(".")[0]
                func_name = stub_function.split(".")[1]
                chirp_f = getattr(modal.Cls.from_name(stub_name_worker, cls_name)(), func_name)
                assert isinstance(chirp_f, modal.Function)
                self.modal_chirp_worker_lookup_cache[(stub_name_worker, stub_function)] = chirp_f
                return chirp_f
            except Exception as e:
                print(
                    f"Error getting chirp worker for {item.model_name}: {e}. stub_name, stub_function: {stub_name_worker}, {stub_function}"
                )

    def _validate_input_queue_item(self, item: QueueItem) -> bool:
        """Validate the input queue item to check if the request if valid."""
        if (
            item.metadata.get("task") in ["infill", "infill_intro", "infill_outro"]
        ) and not item.is_infill:
            print(f"WTF is happening with {item.id} -- task is infill but not infill arguments.")
            return False
        # add task validations checks -- we should not be passing in clips if task isn't specified
        if not item.is_cover_condition and item.metadata.get("cover_clip_id", None) is not None:
            print(f"WTF is happening with {item.id} -- not cover but without cover id passed in.")
            return False
        if not item.is_artist_condition and item.metadata.get("artist_clip_id", None) is not None:
            print(f"WTF is happening with {item.id} -- not artist but without artist id passed in.")
            return False
        if (
            item.metadata.get("task") == "extend"
            or item.is_cover_extend
            or item.is_cover_infill
            or item.is_artist_extend
            or item.is_artist_infill
            or item.is_artist_cover_extend
        ) and item.prompt_audio is None:
            print(f"WTF is happening with {item.id} -- task is extend but prompt audio id passed in.")
            return False
        if item.metadata.get("task") == "upsample" and not item.is_upsample:
            print(f"WTF is happening with {item.id} -- task is upsample but without upsample arguments.")
            return False
        if item.is_diff_infill and (
            (
                item.metadata.get("infill_start_s", 0) - item.metadata.get("infill_context_start_s", 0)
                > 29
            )
            or (item.metadata.get("infill_context_end_s", 0) - item.metadata.get("infill_end_s", 0) > 29)
        ):
            print(
                f"WTF is happening with {item.id} -- task is diff infill but infill context is too long."
            )
            return False
        return True

    @modal.method()
    @distributed_trace("redirect_and_generate", CONDUCTOR_DDOG_SERVICE, env_name=DEPLOYMENT_TYPE)
    def redirect_and_generate(
        self, queue_item_json: str, history_prompt: HistoryPrompt | None = None
    ) -> None:
        """This worker will distribute the queue item to the appropriate model worker.

        If it is a continue and needs to fetch the history, it will go to the history loader.
            History loader will pass the item back into redirect_and_generate.
        Otherwise, it will go through augmentation and to the GPT worker directly.

        Args:
            queue_item_json: the queue item in json format.
                Note that this queue item can be at request-level or clip-level.
            history_prompt: the history prompt in HistoryPrompt.
        """
        original_item = QueueItem(**json.loads(queue_item_json))
        logger.debug(f"  {original_item}")
        tracer_current_span = tracer.current_span()
        if tracer_current_span:
            tracer_current_span.set_tag("id", original_item.id)
            tracer_current_span.set_tag("model_name", original_item.model_name)
            tracer_current_span.set_tag("title", original_item.title)
        # we need to do some house keeping first
        if "experiment" in original_item.metadata:
            # why would it even get here!? from sloppyness in studio api, carried over
            print(f"Request {original_item.id}: Remove unwanted 'experiment' key")
            # first correct the db logging
            original_item.metadata["experiment"] = []
            self._notify_queue_status_with_epxerimental_info(original_item)
            print(f"Request {original_item.id}: Correct DB info sent")
            original_item.metadata.pop("experiment")
        if "param_experiment" in original_item.metadata:
            # why would it even get here!? from sloppyness in studio api, carried over
            print(f"Request {original_item.id}: Remove unwanted 'param_experiment' key")
            # first correct the db logging
            original_item.metadata["param_experiment"] = ""
            self._notify_queue_status_with_epxerimental_info(original_item)
            print(f"Request {original_item.id}: Correct DB info sent")
            original_item.metadata.pop("param_experiment")
        if "model_config" in original_item.metadata:
            # why would it even get here!? from sloppyness in studio api, carried over
            print(f"Request {original_item.id}: Remove unwanted 'model_config' key")
            original_item.metadata.pop("model_config")
        if "experiment_version" in original_item.metadata:
            # why would it even get here!? from sloppyness in studio api, carried over
            print(f"Request {original_item.id}: Remove unwanted 'experiment_version' key")
            original_item.metadata.pop("experiment_version")

        # validate the queue item -- should meet expected conditions
        if not self._validate_input_queue_item(original_item):
            for index, clip_id in enumerate(original_item.all_child_clip_ids):
                new_item = original_item.model_copy(deep=True, update={"id": clip_id, "ids": None})
                new_item.notify_progress(
                    {
                        "id": new_item.id,
                        "type": "error",
                        "error_type": "invalid_generation_request",
                    },
                )
            return

        if (
            original_item.prompt_audio
            or original_item.is_cover_condition
            or original_item.is_artist_condition
            or original_item.is_infill
            or original_item.is_diff_infill
            or original_item.is_playlist_condition
            or original_item.is_multi_artist_consistency
            or original_item.is_underpainting
            or original_item.is_overpainting
        ) and history_prompt is None:
            # This is a continue job, without history, we need to fetch the history first
            _ = self.modal_f_history_loader.spawn(
                original_item.model_dump_json(), parent_context=serialize_context()
            )
            logger.info(f"Request {original_item.id}: Spawned modal job into history loader")
            # nothing to be done for here, job isn't in queue yet, except upsample / stem
            if (original_item.is_upsample or original_item.is_stem) and original_item.all_child_clip_ids:
                for clip_id in original_item.all_child_clip_ids:
                    curr_item = original_item.model_copy(deep=True, update={"id": clip_id, "ids": None})
                    # empty the multi_ids as well
                    curr_item.metadata["multi_ids"] = []
                    # we need to copy the image -- note that this is independent of the job queue
                    self.copy_and_upload_image(curr_item)
            return
        # ok we have everything, let's proceed!
        if not original_item.is_image_to_song:
            # start the image runner -- this is done at request level, image worker will split to clips
            # note that this is one image gen request, single/multiple clip's image gen
            # Check if image generation was already started from the early callback
            if original_item.metadata.get("image_generation_started", False):
                print(f"Request {original_item.id}: Image generation already started, skipping")
            else:
                if original_item.is_upsample or original_item.is_stem:
                    # we do not want to run the image generator for upsample
                    # it is copied after the request is spawned
                    pass
                elif isinstance(original_item.model_name, str) and (
                    "auk" in original_item.model_name
                    or "bluejay" in original_item.model_name
                    or "v4" in original_item.model_name
                ):
                    n_backlog = self.modal_f_pro_image_generator.get_current_stats().backlog
                    if n_backlog < 25:  # reduce the backlog to 25
                        _ = self.modal_f_pro_image_generator.spawn(
                            original_item.model_dump_json(), parent_context=serialize_context()
                        )
                    else:
                        _ = self.modal_f_image_generator.spawn(
                            original_item.model_dump_json(), parent_context=serialize_context()
                        )
                    if "auk" in original_item.model_name or "bluejay" in original_item.model_name:
                        _ = self.modal_f_downsampler.spawn(
                            original_item.model_dump_json(), parent_context=serialize_context()
                        )
                else:
                    _ = self.modal_f_image_generator.spawn(
                        original_item.model_dump_json(), parent_context=serialize_context()
                    )

        # Do the A/B testing split here
        item = self._experimental_model_mixer(original_item)
        # if this is a clip level job -- once we update all workers these can be removed
        # but if they are here, they are backwards compatible
        if not item.all_child_clip_ids:
            if item.model_name not in MODAL_FNS:
                # this is a bad model name, we should notify at the request level
                item.notify_progress(
                    {
                        "id": item.id,
                        "type": "error",
                        "error_type": "invalid_model_name",
                    },
                )
                return
            # start the audio runner, do a look up first
            gpt_worker = self._get_gpt_worker(item)
            # directly goes to the GPT worker, with empty HistoryPrompt
            _ = gpt_worker.spawn(
                item.model_dump_json(), history_prompt, parent_context=serialize_context()
            )
            logger.info(f"Request {item.id}: Spawned modal job {gpt_worker} item id {item.id}")
            self._notify_queue_status_with_epxerimental_info(item)
        # if this is a request level job:
        else:
            # we split the request back to clips level and pass them on
            items_to_notify = []
            for index, clip_id in enumerate(item.child_primary_clip_ids):
                new_item = item.model_copy(deep=True, update={"id": clip_id, "ids": None})
                if item.multi_ids:  # for stems, select the appropriate ids
                    new_item.metadata["multi_ids"] = item.multi_ids[index]
                if experiment_models := item.metadata.get("experiment"):
                    # reset the new_item's model name
                    new_item.model_name = experiment_models[index]
                    print(
                        f"Request {new_item.id}: Created new item {clip_id} with model name: {new_item.model_name}"
                    )
                if new_item.model_name not in MODAL_FNS:
                    # this is a bad model name, we should notify at the request level
                    print(
                        f"Request {new_item.id}: Incorrect model name {new_item.model_name} for clip {clip_id}"
                    )
                    new_item.notify_progress(
                        {
                            "id": new_item.id,
                            "type": "error",
                            "error_type": "invalid_model_name",
                        },
                    )
                    return
                if experiment_inference_config := item.metadata.get("model_config"):
                    # reset the new_item's model name
                    new_item.metadata["model_config"] = experiment_inference_config[index]
                    if not new_item.metadata["model_config"]:
                        # one of the experiment inference could be empty config
                        # in such cases, we don't bother passing them in
                        new_item.metadata.pop("model_config")
                        # this is what we log into the db. we need this to make db logging work.
                        new_item.metadata.pop("param_experiment")
                    else:
                        print(
                            f"Request {new_item.id}: Created new item {clip_id} with experimental inference config: {new_item.metadata['model_config']}"
                        )
                    # for tag augmentation, the augmented tags are passed from studio api
                    # we will overwrite the tags with augmented_tags
                    # the augmented tags are saved in the metadata
                    # the clips triggered with the tag_aug will have 'tag_aug_vx' in the param_experiment
                    if new_item.metadata.get("model_config", {}).get("tag_aug", False):
                        if new_augmented_tags := new_item.metadata.get("augmented_tags"):
                            print(
                                f"Request {new_item.id}: Created new item {clip_id} with "
                                f"experimental augment tags from: "
                                f"{new_item.metadata['tags']} to "
                                f"{new_augmented_tags}."
                            )
                            new_item.metadata["model_config"] = {}
                            new_item.metadata["tags"] = new_augmented_tags
                        else:
                            # augmented tag not implemented
                            new_item.metadata.pop("model_config")
                            new_item.metadata.pop("param_experiment")

                # start the audio runner, do a look up first
                gpt_worker = self._get_gpt_worker(new_item)
                # for delay experiment -- only sleep for the first request
                if (delay_request_time := new_item.metadata.get("exp_delay_by_sec")) and index == 0:
                    print(
                        f"Request {new_item.id}: is delayed by {delay_request_time}s for user {new_item.metadata.get('user_id')}"
                    )
                    time.sleep(delay_request_time)
                # extra traffic monitoring
                if (
                    DEPLOYMENT_TYPE == "prod"
                    and new_item.model_name
                    and (
                        "v4" in new_item.model_name
                        or "auk" in new_item.model_name
                        or "bluejay" in new_item.model_name
                    )
                ):
                    # check backlog size
                    n_backlog = gpt_worker.get_current_stats().backlog
                    if n_backlog > 200:  # be a little more generous about backlog size
                        print(
                            f"Request {new_item.id}: Current backlog for {new_item.model_name}: {gpt_worker.get_current_stats().backlog}"
                        )
                        new_item.notify_progress(
                            {
                                "id": new_item.id,
                                "type": "error",
                                "error_type": "moderation_failure",
                                "error_message": "Sorry but we are currently experiencing elevated usage. Please try again later. Thanks!",
                            },
                        )
                        # can't pass the info down the line
                        return
                # spin up the gpt worker
                _ = gpt_worker.spawn(
                    new_item.model_dump_json(), history_prompt, parent_context=serialize_context()
                )
                logger.debug(
                    f"Request {new_item.id}: Spawned modal job {gpt_worker}, clip id {new_item.id}"
                )
                items_to_notify.append(new_item)
                self.events_queue.put(
                    {
                        "type": "generate_queued",
                        "data": {
                            "clip_id": new_item.id,
                        },
                    },
                    partition=item.id,
                    partition_ttl=60,
                )
            # cache the items and notify them at the end to not blocking the job spawning
            for curr_item in items_to_notify:
                self._notify_queue_status_with_epxerimental_info(curr_item)
                # note that we do it after the generation request is spawned
                logger.debug(
                    f"Request {new_item.id}: Finished notify clip id {curr_item.id} with model name: {curr_item.model_name}"
                )


def _test_get_prompt_from_gpt_description_prompt(chat_gpt_app: PromptGenerationStub) -> None:
    nice_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "gpt_description_prompt": "a cumbia song about a cowboy and his cow",
            },
        )
    ).model_dump_json()
    response_dict = chat_gpt_app.get_prompt_from_gpt_description_prompt.remote(nice_prompt)
    print("response_dict:", response_dict)
    partial_expected_response_dict = {
        "id": "1337",
        "ok": 1,
    }
    for k in partial_expected_response_dict:
        assert partial_expected_response_dict[k] == response_dict[k], (
            k,
            partial_expected_response_dict[k],
            response_dict[k],
        )
    assert "[verse" in response_dict["prompt"].lower(), response_dict["prompt"]
    assert isinstance(response_dict["genre_tags"][0], str)


def _test_get_prompt_from_gpt_description_prompt_together_ai(chat_gpt_app: PromptGenerationStub) -> None:
    together_model = (
        "patsuno/Meta-Llama-3.1-8B-Instruct-Reference-remi-test-8B-lr-1en4-ep7-CORRECTED-fdb36cb0"
    )
    nice_prompt = (
        QueueItem(
            id="1337",
            model_name=together_model,
            metadata={
                "gpt_description_prompt": "a sad country song about a cowboy and his cow",
            },
        )
    ).model_dump_json()
    response_dict = chat_gpt_app.get_prompt_from_gpt_description_prompt.remote(nice_prompt)
    print("response_dict:", response_dict)
    partial_expected_response_dict = {
        "id": "1337",
        "ok": 1,
    }
    for k in partial_expected_response_dict:
        assert partial_expected_response_dict[k] == response_dict[k], (
            k,
            partial_expected_response_dict[k],
            response_dict[k],
        )
    assert "[verse" in response_dict["prompt"].lower(), response_dict["prompt"]
    assert isinstance(response_dict["genre_tags"][0], str)


def _test_get_prompt_from_gpt_description_prompt_remi(chat_gpt_app: PromptGenerationStub) -> None:
    # TODO(pat) refactor these tests
    nice_prompt = (
        QueueItem(
            id="1337",
            metadata={
                "gpt_description_prompt": "a sad country song about a cowboy and his cow",
                "lyrics_model": "remi-v1",
            },
        )
    ).model_dump_json()
    response_dict = chat_gpt_app.get_prompt_from_gpt_description_prompt.remote(nice_prompt)
    print("response_dict:", response_dict)
    partial_expected_response_dict = {
        "id": "1337",
        "ok": 1,
    }
    for k in partial_expected_response_dict:
        assert partial_expected_response_dict[k] == response_dict[k], (
            k,
            partial_expected_response_dict[k],
            response_dict[k],
        )
    assert "[verse" in response_dict["prompt"].lower(), response_dict["prompt"]
    assert isinstance(response_dict["genre_tags"][0], str)


def _test_get_prompt_from_gpt_description_prompt_modal_remi(chat_gpt_app: PromptGenerationStub) -> None:
    # TODO(pat) refactor these tests
    modal_remi_model = "my-remi-8b-1"
    nice_prompt = (
        QueueItem(
            id="1337",
            metadata={
                "gpt_description_prompt": "a sad country song about a cowboy and his cow",
                "lyrics_model": modal_remi_model,
            },
        )
    ).model_dump_json()
    response_dict = chat_gpt_app.get_prompt_from_gpt_description_prompt.remote(nice_prompt)
    print("response_dict:", response_dict)
    partial_expected_response_dict = {
        "id": "1337",
        "ok": 1,
    }
    for k in partial_expected_response_dict:
        assert partial_expected_response_dict[k] == response_dict[k], (
            k,
            partial_expected_response_dict[k],
            response_dict[k],
        )
    assert "[verse" in response_dict["prompt"].lower(), response_dict["prompt"]
    assert isinstance(response_dict["genre_tags"][0], str)


def _test_get_prompt_from_gpt_description_prompt_new_chatgpt(chat_gpt_app: PromptGenerationStub) -> None:
    # TODO(pat) refactor these tests
    new_chatgpt = "gpt-4o-2024-11-20"
    nice_prompt = (
        QueueItem(
            id="1337",
            metadata={
                "gpt_description_prompt": "a sad country song about a cowboy and his cow",
                "lyrics_model": new_chatgpt,
            },
        )
    ).model_dump_json()
    response_dict = chat_gpt_app.get_prompt_from_gpt_description_prompt.remote(nice_prompt)
    print("response_dict:", response_dict)
    partial_expected_response_dict = {
        "id": "1337",
        "ok": 1,
    }
    for k in partial_expected_response_dict:
        assert partial_expected_response_dict[k] == response_dict[k], (
            k,
            partial_expected_response_dict[k],
            response_dict[k],
        )
    assert "[verse" in response_dict["prompt"].lower(), response_dict["prompt"]
    assert isinstance(response_dict["genre_tags"][0], str)


def _test_get_prompt_from_gpt_description_prompt_vertex(chat_gpt_app: PromptGenerationStub) -> None:
    # TODO(pat) refactor these tests
    model = "gemini-2.0"
    nice_prompt = (
        QueueItem(
            id="1337",
            metadata={
                "gpt_description_prompt": "a sad country song about a cowboy and his cow",
                "lyrics_model": model,
            },
        )
    ).model_dump_json()
    response_dict = chat_gpt_app.get_prompt_from_gpt_description_prompt.remote(nice_prompt)
    print("response_dict:", response_dict)
    partial_expected_response_dict = {
        "id": "1337",
        "ok": 1,
    }
    for k in partial_expected_response_dict:
        assert partial_expected_response_dict[k] == response_dict[k], (
            k,
            partial_expected_response_dict[k],
            response_dict[k],
        )
    assert "[verse" in response_dict["prompt"].lower(), response_dict["prompt"]
    assert isinstance(response_dict["genre_tags"][0], str)


def _test_get_prompt_from_gpt_description_prompt_remi_hps(chat_gpt_app: PromptGenerationStub) -> None:
    # TODO(pat) refactor these tests
    remi_model = "remi-v1-hps1"
    nice_prompt = (
        QueueItem(
            id="1337",
            metadata={
                "gpt_description_prompt": "a sad country song about a cowboy and his cow",
                "lyrics_model": remi_model,
            },
        )
    ).model_dump_json()
    response_dict = chat_gpt_app.get_prompt_from_gpt_description_prompt.remote(nice_prompt)
    print("response_dict:", response_dict)
    partial_expected_response_dict = {
        "id": "1337",
        "ok": 1,
    }
    for k in partial_expected_response_dict:
        assert partial_expected_response_dict[k] == response_dict[k], (
            k,
            partial_expected_response_dict[k],
            response_dict[k],
        )
    assert "[verse" in response_dict["prompt"].lower(), response_dict["prompt"]
    assert isinstance(response_dict["genre_tags"][0], str)


def _test_moderate_and_generate(chat_gpt_app: ChatGptStub) -> None:
    nice_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "gpt_description_prompt": "a pop song about a literal banana",
            },
            title="Write me a song",
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(nice_prompt)
    assert item is not None


def _test_moderate_and_generate_stem(chat_gpt_app: ChatGptStub) -> None:
    id_a = "1338a"
    id_b = "1338b"
    a_ids = [id_a + "_stem", id_a + "_complement"]
    b_ids = [id_b + "_stem", id_b + "_complement"]
    nice_prompt = (
        QueueItem(
            id=id_a,
            prompt_audio="a5e2198a-f352-4abb-9a24-7f81b143ded3",  # stone
            model_name="chirp-stem-comp",
            metadata={
                "task": "gen_stem",
                "stem_type_group_name": "Vocals",
                "multi_ids": [a_ids, b_ids],
            },
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(nice_prompt)
    assert item is not None


def _test_moderate_and_generate_stem_multi(chat_gpt_app: ChatGptStub) -> None:
    id_a = "1339"
    a_ids = [f"{id_a}_{i}" for i in range(12)]
    b_ids = [f"{id_a}_{i}" for i in range(12, 24)]
    nice_prompt = (
        QueueItem(
            id=id_a,
            prompt_audio="a5e2198a-f352-4abb-9a24-7f81b143ded3",  # stone
            model_name="chirp-stem",
            metadata={
                "task": "gen_stem",
                "multi_ids": [a_ids, b_ids],
            },
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(nice_prompt)
    assert item is not None


def _test_moderate_and_generate_extract_single_image(chat_gpt_app: ChatGptStub) -> None:
    nice_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "gpt_description_prompt": "Suno iOS app launch",
                "image_to_song_s3_ids": ["c509b8b5"],
            },
            title="Friday",
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(nice_prompt)
    assert item is not None


def _test_moderate_and_generate_extract_multiple_images(chat_gpt_app: ChatGptStub) -> None:
    nice_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "gpt_description_prompt": "Suno iOS app launch",
                "image_to_song_s3_ids": [
                    "c509b8b5",
                    "c509b8b6",
                ],  # likely error is rate limiting by HIVE for the same image url!
            },
            title="Friday",
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(nice_prompt)
    assert item is not None


def _test_moderate_and_generate_naughty_gpt_description_prompt(chat_gpt_app: ChatGptStub) -> None:
    naughty_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                # what a world
                "gpt_description_prompt": "What the fuck did you say to me, you little bitch?  You're fucking dead, kiddo.",
            },
            title="Write me a song",
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(naughty_prompt)
    assert item is None


def _test_moderate_and_generate_foreign_languages(chat_gpt_app: ChatGptStub) -> None:
    gpt_prompts = {
        "spanish": "una cancion pop sobre un platano literal",
        "spanish2": "una canción de amor sobre España",
        "chinese": "一首关于真正的香蕉的歌",
        "japanese": "文字通りのバナナについてのポップソング",
        "portuguese": "uma música dubstep sobre uma banana literal",
        "arabic": "أغنية روك عن موزة حرفية",
        "hindi": "एक शाब्दिक केले के बारे में एक रॉक गीत",
        "urdu": "لفظی کیلے کے بارے میں ایک ملکی گانا",
        "russian": "рэп-песня о банане в буквальном смысле слова",
        "dutch": "ik wil een liedje over een bananan vla met stukjes aardbei",
    }
    for language, prompt in gpt_prompts.items():
        nice_prompt = (
            QueueItem(
                id="1337",
                model_name=TEST_MODEL_NAME,
                metadata={
                    "gpt_description_prompt": prompt,
                },
            )
        ).model_dump_json()
        raw_item = chat_gpt_app.moderate_and_generate.remote(nice_prompt)
        assert raw_item is not None
        item = json.loads(raw_item)
        tags = item["metadata"]["tags"]
        title = item["title"]
        assert title
        assert tags
        assert item["title"] != tags
        assert item["prompt_text"]
        # these next two tests might be a little too strict, tags don't
        # necessarily need to come back in English.  feel free to
        # revise if failing
        lowercased_lyrics = item["prompt_text"].lower()
        assert "verse" in lowercased_lyrics or "verso" in lowercased_lyrics

        print(language, prompt, item["prompt_text"])
        assert len(item["prompt_text"]) > 100
        assert len(item["prompt_text"].split("\n")) >= 10


def _test_moderate_generate_ok_title(chat_gpt_app: ChatGptStub) -> None:
    ok_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "prompt": "please write me a song",
            },
            prompt_text="Please write a song",
            title="I Love You",
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(ok_prompt)
    assert item is not None


def _test_moderate_generate_bad_title(chat_gpt_app: ChatGptStub) -> None:
    bad_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "prompt": "please write me a song",
            },
            prompt_text="Please write a song",
            title="Fuck you, you are the worst",
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(bad_prompt)
    assert item["moderation_result"] == "success", item


def _test_moderate_text_ok(chat_gpt_app: TextModerationSub) -> None:
    ok_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "text": "hello how are you",
            },
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_text.remote(ok_prompt)
    assert item["moderation_result"] == "success", item


def _test_moderate_text_moderately_naughty(chat_gpt_app: TextModerationSub) -> None:
    ok_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "text": "fuck fuck fuck",
            },
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_text.remote(ok_prompt)
    assert item["moderation_result"] == "success", item


_MARTYROLOGY_LYRICS = """
[Chorus]
After having done a superhuman good,
Emperor Tal Julian arrived,
He sent him to prison!
After a while he tortured him!
They began by cutting off his hand

[Chorus]
Then he made him swallow molten lead,
Sticking it down his throat by the snout,
On the gridiron frustrated!
From snakes poisoned!
And immersed in the hottest bitumen…

But he only died with a blow!
"""


def _test_moderate_text_martyrology(chat_gpt_app: TextModerationSub) -> None:
    ok_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "text": _MARTYROLOGY_LYRICS,
            },
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_text.remote(ok_prompt)
    assert item["moderation_result"] == "success", item


def _test_moderate_and_generate_martyrology(chat_gpt_app: ChatGptStub) -> None:
    prompt = (
        QueueItem(id="1337", model_name=TEST_MODEL_NAME, metadata={}, prompt_text=_MARTYROLOGY_LYRICS)
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(prompt)
    assert item is not None


def _test_moderate_text_very_naughty(chat_gpt_app: TextModerationSub) -> None:
    bad_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "text": "I'm going to murder you",
            },
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_text.remote(bad_prompt)
    assert item["moderation_result"] == "failure", item


def _test_disable_artist_name_detection(chat_gpt_app: ChatGptStub) -> None:
    artist_names_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "gpt_description_prompt": "a song about Ed Sheeran, Michael Jackson and the Beastie Boys",
                "check_artist_names": False,
            },
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(artist_names_prompt)
    assert item is not None


def _test_artist_name_detection_tags(chat_gpt_app: ChatGptStub) -> None:
    artist_names_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "tags": "taylor swift",
            },
            prompt_text="i am testing",
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(artist_names_prompt)
    assert item is None, item


def _test_artist_name_detection(chat_gpt_app: ChatGptStub) -> None:
    artist_names_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={"gpt_description_prompt": "a song about ed sheeran"},
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(artist_names_prompt)
    assert item is None


def _test_one_box_instrumentals(chat_gpt_app: ChatGptStub) -> None:
    instrumental_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={"gpt_description_prompt": "an instrumental funk song", "make_instrumental": True},
        )
    ).model_dump_json()
    item_str = chat_gpt_app.moderate_and_generate.remote(instrumental_prompt)
    item = json.loads(item_str)
    assert item["prompt_text"] == "[Instrumental]", item
    assert not (item["metadata"].get("lang")), item


def _test_no_genres_in_lyrics(chat_gpt_app: ChatGptStub) -> None:
    # This test is a little flaky, feel free to ignore transient errors.
    errors = 0
    for i in range(5):
        prompt = (
            QueueItem(
                id="1337",
                model_name=TEST_MODEL_NAME,
                metadata={
                    "gpt_description_prompt": "a synthpop song about a lost dog",
                },
            )
        ).model_dump_json()
        item_str = chat_gpt_app.moderate_and_generate.remote(prompt)
        item = json.loads(item_str)
        print("synthpop item:", item)
        if "synthpop" in item["prompt_text"]:
            errors += 1
    assert errors <= 2, errors


def _test_lyrics_moderation(chat_gpt_app: ChatGptStub) -> None:
    bad_prompt = "dink"
    queue_item = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={},
            # custom lyrics moderation ignores metadata.prompt and reads prompt_text instead
            prompt_text=bad_prompt,
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(queue_item)
    assert item is None, item


def _test_lyrics_length(chat_gpt_app: ChatGptStub) -> None:
    v3_queue_item = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "gpt_description_prompt": "a song about three scoops of vanilla yogurt",
            },
            prompt_text="",
        )
    ).model_dump_json()
    item_str = chat_gpt_app.moderate_and_generate.remote(v3_queue_item)
    item = json.loads(item_str)
    prompt = item["prompt_text"]
    stanzas = get_stanzas(prompt)
    assert len(stanzas) == 3, (prompt, len(stanzas))

    v2_queue_item = (
        QueueItem(
            id="1337",
            model_name="chirp-v2-xxl-alpha",
            metadata={
                "gpt_description_prompt": "a song about two scoops of vanilla yogurt",
            },
            prompt_text="",
        )
    ).model_dump_json()
    item_str = chat_gpt_app.moderate_and_generate.remote(v2_queue_item)
    item = json.loads(item_str)
    prompt = item["prompt_text"]
    stanzas = get_stanzas(prompt)
    assert len(stanzas) == 6, (prompt, len(stanzas))

    unspecified_queue_item = (
        QueueItem(
            id="1337",
            metadata={
                "gpt_description_prompt": "a song about unspecified scoops of vanilla yogurt",
            },
            prompt_text="",
            model_name="chirp-v3",
        )
    ).model_dump_json()
    item_str = chat_gpt_app.moderate_and_generate.remote(unspecified_queue_item)
    item = json.loads(item_str)
    prompt = item["prompt_text"]
    stanzas = get_stanzas(prompt)
    assert len(stanzas) == 3, (prompt, len(stanzas))


def _test_lyrics_length_model_3p5(chat_gpt_app: ChatGptStub) -> None:
    v3_queue_item = (
        QueueItem(
            id="1337",
            model_name="chirp-v3p5",
            metadata={
                "gpt_description_prompt": "a song about three scoops of vanilla yogurt",
            },
            prompt_text="",
        )
    ).model_dump_json()
    item_str = chat_gpt_app.moderate_and_generate.remote(v3_queue_item)
    assert item_str is not None
    item = json.loads(item_str)
    prompt = item["prompt_text"]
    stanzas = get_stanzas(prompt)
    # we ask for six stanzas, but sometimes chatGPT gets tired, so
    # just make sure we get at least 5 back.
    assert len(stanzas) >= 5, (prompt, len(stanzas))


def _test_image_extraction_screenshot(chat_gpt_app: ChatGptStub) -> None:
    nice_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "gpt_description_prompt": "festival",
                "image_to_song_s3_ids": ["image_01872ae0-65fe-4549-b514-78bb3ec98299"],
            },
            title="N/A",
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(nice_prompt)
    assert item is not None


def _test_predict_lyrics_language(chat_gpt_app: ChatGptStub) -> None:
    english_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            prompt_text="""
            I am testing this is English

            I am testing this is English
            """,
            metadata={},
            title="N/A",
            ids=[],
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(english_prompt)
    assert item is not None
    item_language = json.loads(item)["metadata"].get("lang")
    assert item_language == "English"
    chinese_prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            prompt_text="""
            我在说中文

            我在说中文

            I am testing this is English
            """,
            metadata={},
            title="N/A",
            ids=[],
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(chinese_prompt)
    assert item is not None
    item_language = json.loads(item)["metadata"].get("lang")
    assert item_language == "Chinese"


def _test_copyright_detector(copyright_detector_app):
    good_lyrics = "hi how are you"
    assert not copyright_detector_app.are_lyrics_copyrighted.remote(good_lyrics)
    bad_lyrics = "beat it (beat it), beat it (beat it), no one wants to be defeated"
    assert copyright_detector_app.are_lyrics_copyrighted.remote(bad_lyrics)
    print("copyright detection tests completed")


def _test_prompt_upsampler(prompt_upsampler_app):
    original_prompt = "lo fi japanese citypop"
    upsampled_prompt = prompt_upsampler_app.upsample.remote(original_prompt)
    expected_terms = ["drums", "jazz", "piano", "guitar"]
    found_at_least_one_term = any(term in upsampled_prompt for term in expected_terms)
    assert (
        isinstance(upsampled_prompt, str)
        and len(upsampled_prompt) > len(original_prompt)
        and found_at_least_one_term
    ), upsampled_prompt

    original_prompt = "lo fi japanese citypop"
    upsampled_prompt = prompt_upsampler_app.upsample.remote(original_prompt, is_instrumental=False)
    expected_terms = ["drums", "jazz", "piano", "guitar"]
    found_at_least_one_term = any(term in upsampled_prompt for term in expected_terms)
    assert (
        isinstance(upsampled_prompt, str)
        and len(upsampled_prompt) > len(original_prompt)
        and found_at_least_one_term
    ), upsampled_prompt

    original_prompt = "lo fi japanese citypop"
    upsampled_prompt = prompt_upsampler_app.upsample.remote(original_prompt, is_instrumental=True)
    expected_terms = ["drums", "jazz", "piano", "guitar"]
    found_at_least_one_term = any(term in upsampled_prompt for term in expected_terms)
    assert (
        isinstance(upsampled_prompt, str)
        and len(upsampled_prompt) > len(original_prompt)
        and found_at_least_one_term
    ), upsampled_prompt

    assert "vocals" not in upsampled_prompt, upsampled_prompt

    original_prompt = "taylor swift"
    upsampled_prompt = prompt_upsampler_app.upsample.remote(
        original_prompt, is_instrumental=True, check_artist_names=False
    )
    assert isinstance(upsampled_prompt, str)

    original_prompt = "taylor swift"
    result = prompt_upsampler_app.upsample.remote(
        original_prompt, is_instrumental=True, check_artist_names=True
    )
    assert result["ok"] is False, upsampled_prompt
    assert "taylor" in result["error_message"], upsampled_prompt

    original_prompt = "jazzwave"
    result = prompt_upsampler_app.upsample.remote(
        original_prompt, is_instrumental=True, check_artist_names=True
    )
    assert result["ok"] is True, upsampled_prompt
    assert "jazz" in result["result"]


def _test_prompt_downsampler(prompt_downsampler_app):
    long_prompt = (
        "A vibrant blend of melodic hip hop and pop influences, showcasing an emotional journey "
        "through love and longing. The sound is characterized by rich synthesizers, rhythmic bass, "
        "and danceable beats, all enhancing the energetic vibe of the track. Vocally, it showcases "
        "smooth male vocals that project raw emotion and nostalgia, inviting listeners to connect "
        "with the feelings of lost love. The song thrives on its catchy chorus, engaging the audience "
        "throughout, with a reflective progression that maintains interest and engagement"
    )
    downsampled = prompt_downsampler_app.downsample.remote(long_prompt)
    assert isinstance(downsampled, str)
    assert any(genre in downsampled.lower() for genre in ["hip hop", "pop", "ballad"]), downsampled


def _test_twitter_to_song(chat_gpt_app: ChatGptStub):
    prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "request_log_id": "6a8321c1-579a-4b48-9a89-403d03f476fc",
                "tweet_id": "1912645166415880694",
                "credit_cost": 5,
                "feature_flags": 3,
                "user_handle": "trolltrudy",
                "created_session_id": None,
                "twitter_to_song_log_id": "6a8321c1-579a-4b48-9a89-403d03f476fc",
                "twitter_to_song_content": "@PopCrave Can we send her back",
                "type": "gen",
                "source": "web",
                "prompt": "",
                "tags": "funny, pop, pop music",
                "gpt_prompt": None,
                "gpt_description_prompt": "@PopCrave Can we send her back\n\nTop Comments:\n\n\n@n1k1s_son @Wendys @PopCrave Over 2 years old, scumbag https://t.co/Oj4W05SF6m\n\n@kinematicaI @PopCrave Again. Don't know. Don't care.\n\n@Wendys @PopCrave @trolltrudy  Wendy's wanna send Katy Perry back to Space loooool\n\n@Wendys @PopCrave You are playing on the wrong side Wenzy 🙃\n\n@Powers_HK @Wendys @PopCrave Violence?",
            },
        )
    ).model_dump_json()
    item = chat_gpt_app.moderate_and_generate.remote(prompt)
    assert item is not None


def _test_generate_lyrics_and_genre_from_gpt_instructions(chat_gpt_app: ChatGptStub):
    prompt = (
        QueueItem(
            id="1337",
            model_name=TEST_MODEL_NAME,
            metadata={
                "request_log_id": "6a8321c1-579a-4b48-9a89-403d03f476fc",
                "tweet_id": "1912645166415880694",
                "credit_cost": 5,
                "feature_flags": 3,
                "user_handle": "trolltrudy",
                "created_session_id": None,
                "twitter_to_song_log_id": "6a8321c1-579a-4b48-9a89-403d03f476fc",
                "twitter_to_song_content": "@PopCrave Can we send her back",
                "type": "gen",
                "source": "web",
                "prompt": "",
                "tags": "funny, pop, pop music",
                "gpt_prompt": None,
                "gpt_description_prompt": "@PopCrave Can we send her back\n\nTop Comments:\n\n\n@n1k1s_son @Wendys @PopCrave Over 2 years old, scumbag https://t.co/Oj4W05SF6m\n\n@kinematicaI @PopCrave Again. Don't know. Don't care.\n\n@Wendys @PopCrave @trolltrudy  Wendy's wanna send Katy Perry back to Space loooool\n\n@Wendys @PopCrave You are playing on the wrong side Wenzy 🙃\n\n@Powers_HK @Wendys @PopCrave Violence?",
            },
        )
    ).model_dump_json()
    item = chat_gpt_app.generate_lyrics_and_genre_from_gpt_instructions.remote(prompt)
    assert item is not None


@app.local_entrypoint()
def main() -> None:
    """Test ChatGptStub locally."""
    copyright_detector_app = CopyrightDetectorStub()
    chat_gpt_app = ChatGptStub()
    prompt_generation_app = PromptGenerationStub()
    text_moderation_app = TextModerationSub()
    prompt_upsampler_app = PromptUpsamplerStub()
    prompt_downsampler_app = PromptDownsamplerStub()

    _test_prompt_upsampler(prompt_upsampler_app)
    _test_prompt_downsampler(prompt_downsampler_app)
    _test_copyright_detector(copyright_detector_app)
    _test_get_prompt_from_gpt_description_prompt_new_chatgpt(prompt_generation_app)
    _test_get_prompt_from_gpt_description_prompt_remi_hps(prompt_generation_app)
    _test_get_prompt_from_gpt_description_prompt_modal_remi(prompt_generation_app)

    _test_get_prompt_from_gpt_description_prompt_remi(prompt_generation_app)
    _test_get_prompt_from_gpt_description_prompt_together_ai(prompt_generation_app)
    _test_get_prompt_from_gpt_description_prompt(prompt_generation_app)
    _test_moderate_and_generate(chat_gpt_app)
    _test_moderate_and_generate_naughty_gpt_description_prompt(chat_gpt_app)
    _test_moderate_and_generate_stem(chat_gpt_app)
    _test_moderate_and_generate_stem_multi(chat_gpt_app)

    _test_moderate_and_generate_foreign_languages(chat_gpt_app)

    _test_moderate_generate_ok_title(chat_gpt_app)

    _test_moderate_text_ok(text_moderation_app)
    _test_moderate_text_moderately_naughty(text_moderation_app)
    _test_moderate_text_martyrology(text_moderation_app)
    _test_moderate_and_generate_martyrology(chat_gpt_app)
    _test_moderate_text_very_naughty(text_moderation_app)
    _test_artist_name_detection(chat_gpt_app)
    _test_disable_artist_name_detection(chat_gpt_app)
    _test_artist_name_detection_tags(chat_gpt_app)
    _test_one_box_instrumentals(chat_gpt_app)
    _test_lyrics_moderation(chat_gpt_app)
    _test_lyrics_length(chat_gpt_app)

    _test_no_genres_in_lyrics(chat_gpt_app)

    _test_lyrics_length_model_3p5(chat_gpt_app)
    _test_predict_lyrics_language(chat_gpt_app)
    _test_twitter_to_song(chat_gpt_app)
    _test_generate_lyrics_and_genre_from_gpt_instructions(chat_gpt_app)
