#  Copyright (C) 2025. Hao Zheng
#  All rights reserved.

import concurrent.futures
import json
import shutil
import traceback
import warnings
from copy import deepcopy
from dataclasses import dataclass
from pathlib import Path
from pprint import pformat
from queue import Queue
from threading import Lock
from typing import Any

from faster_whisper.transcribe import Segment

from openlrc.context import TranslateInfo
from openlrc.defaults import default_asr_options, default_preprocess_options, default_vad_options
from openlrc.logger import logger
from openlrc.models import ModelConfig
from openlrc.opt import SubtitleOptimizer
from openlrc.preprocess import Preprocessor
from openlrc.subtitle import BilingualSubtitle, Subtitle
from openlrc.transcribe import Transcriber
from openlrc.translate import LLMTranslator
from openlrc.utils import (
    Timer,
    extend_filename,
    extract_audio,
    format_timestamp,
    get_audio_duration,
    get_file_type,
    get_preprocessed_path,
)

_SENTINEL = object()


@dataclass
class TranscriptionConfig:
    """
    Configuration for the transcription stage.

    Args:
        whisper_model: Name of whisper model. Default: ``large-v3``
        compute_type: Computation type (``default``, ``int8``, ``int8_float16``,
            ``int16``, ``float16``, ``float32``). Default: ``float16``
        device: Device for computation. Default: ``cuda``
        asr_options: Parameters for whisper model.
        vad_options: Parameters for VAD model.
        preprocess_options: Options for audio preprocessing.
    """

    whisper_model: str = "large-v3"
    compute_type: str = "float16"
    device: str = "cuda"
    asr_options: dict | None = None
    vad_options: dict | None = None
    preprocess_options: dict | None = None


@dataclass
class TranslationConfig:
    """
    Configuration for the translation stage.

    Args:
        chatbot_model: The chatbot model to use. Can be a string like
            ``'gpt-4.1-nano'`` or ``'provider:model-name'``, or a ``ModelConfig``
            instance. Default: ``gpt-4.1-nano``
        fee_limit: Maximum fee per translation call in USD. Default: ``0.8``
        consumer_thread: Number of parallel translation threads. Default: ``4``
        proxy: Proxy for API requests. e.g. ``'http://127.0.0.1:7890'``
        base_url_config: Base URL dict for OpenAI & Anthropic.
        glossary: Dictionary or path mapping source words to translations.
        retry_model: Fallback model for translation retries.
        is_force_glossary_used: Force glossary usage in context. Default: ``False``
    """

    chatbot_model: str | ModelConfig = "gpt-4.1-nano"
    fee_limit: float = 0.8
    consumer_thread: int = 4
    proxy: str | None = None
    base_url_config: dict | None = None
    glossary: dict | str | Path | None = None
    retry_model: str | ModelConfig | None = None
    is_force_glossary_used: bool = False


class LRCer:
    """
    Orchestrator for audio/video transcription and translation.

    Preferred usage (config objects)::

        lrcer = LRCer(
            transcription=TranscriptionConfig(whisper_model='large-v3', device='cuda'),
            translation=TranslationConfig(chatbot_model='gpt-4.1-nano', fee_limit=1.0),
        )

    Legacy keyword arguments are still accepted but deprecated and will be
    removed in a future release::

        # Deprecated — emits DeprecationWarning
        lrcer = LRCer(whisper_model='large-v3', chatbot_model='gpt-4.1-nano')
    """

    def __init__(
        self,
        *,
        transcription: TranscriptionConfig | None = None,
        translation: TranslationConfig | None = None,
        # Legacy keyword arguments — deprecated
        whisper_model: Any = _SENTINEL,
        compute_type: Any = _SENTINEL,
        device: Any = _SENTINEL,
        chatbot_model: Any = _SENTINEL,
        fee_limit: Any = _SENTINEL,
        consumer_thread: Any = _SENTINEL,
        asr_options: Any = _SENTINEL,
        vad_options: Any = _SENTINEL,
        preprocess_options: Any = _SENTINEL,
        proxy: Any = _SENTINEL,
        base_url_config: Any = _SENTINEL,
        glossary: Any = _SENTINEL,
        retry_model: Any = _SENTINEL,
        is_force_glossary_used: Any = _SENTINEL,
    ):

        # Detect whether any legacy keyword was explicitly passed.
        legacy_kwargs = {
            "whisper_model": whisper_model,
            "compute_type": compute_type,
            "device": device,
            "chatbot_model": chatbot_model,
            "fee_limit": fee_limit,
            "consumer_thread": consumer_thread,
            "asr_options": asr_options,
            "vad_options": vad_options,
            "preprocess_options": preprocess_options,
            "proxy": proxy,
            "base_url_config": base_url_config,
            "glossary": glossary,
            "retry_model": retry_model,
            "is_force_glossary_used": is_force_glossary_used,
        }
        legacy_used = {k: v for k, v in legacy_kwargs.items() if v is not _SENTINEL}

        if legacy_used:
            warnings.warn(
                "Passing keyword arguments directly to LRCer() is deprecated and will be removed in a future "
                "release. Use TranscriptionConfig / TranslationConfig instead. "
                "See https://github.com/zh-plus/openlrc/issues/81 for migration details.",
                DeprecationWarning,
                stacklevel=2,
            )

        if legacy_used and (transcription is not None or translation is not None):
            raise ValueError(
                "Cannot mix legacy keyword arguments with TranscriptionConfig/TranslationConfig. "
                "Use one style or the other."
            )

        if legacy_used:
            # Build config objects from legacy kwargs, using dataclass defaults for unset values.
            transcription_fields = {
                k: v
                for k, v in legacy_used.items()
                if k in ("whisper_model", "compute_type", "device", "asr_options", "vad_options", "preprocess_options")
            }
            translation_fields = {
                k: v
                for k, v in legacy_used.items()
                if k
                in (
                    "chatbot_model",
                    "fee_limit",
                    "consumer_thread",
                    "proxy",
                    "base_url_config",
                    "glossary",
                    "retry_model",
                    "is_force_glossary_used",
                )
            }
            transcription = TranscriptionConfig(**transcription_fields)
            translation = TranslationConfig(**translation_fields)

        self._transcription_config = transcription or TranscriptionConfig()
        self._translation_config = translation or TranslationConfig()

        # Translation state
        self.chatbot_model = self._translation_config.chatbot_model
        self.fee_limit = self._translation_config.fee_limit
        self.api_fee = 0  # Can be updated in different thread, operation should be thread-safe
        self.from_video = set()
        self.proxy = self._translation_config.proxy
        self.base_url_config = self._translation_config.base_url_config
        self.glossary = self.parse_glossary(self._translation_config.glossary)
        self.retry_model = self._translation_config.retry_model
        self.is_force_glossary_used = self._translation_config.is_force_glossary_used

        self._lock = Lock()
        self.exception = None
        self.consumer_thread = self._translation_config.consumer_thread

        # Merge default options with provided options
        self.asr_options = {**default_asr_options, **(self._transcription_config.asr_options or {})}
        self.vad_options = {**default_vad_options, **(self._transcription_config.vad_options or {})}
        self.preprocess_options = {
            **default_preprocess_options,
            **(self._transcription_config.preprocess_options or {}),
        }

        # Lazy initialization: Transcriber is created on first access via the property.
        self._transcriber_lock = Lock()
        self._transcriber = None
        self.transcribed_paths = []

    @property
    def transcriber(self) -> Transcriber:
        """Lazily initialize and return the Transcriber instance (thread-safe)."""
        if self._transcriber is None:
            with self._transcriber_lock:
                if self._transcriber is None:
                    self._transcriber = Transcriber(
                        model_name=self._transcription_config.whisper_model,
                        compute_type=self._transcription_config.compute_type,
                        device=self._transcription_config.device,
                        asr_options=self.asr_options,
                        vad_options=self.vad_options,
                    )
        return self._transcriber

    @staticmethod
    def parse_glossary(glossary: dict | str | Path | None) -> dict | None:
        if not glossary:
            return None

        if isinstance(glossary, dict):
            return glossary

        glossary_path = Path(glossary)
        if not glossary_path.exists():
            logger.warning("Glossary file not found.")
            return None

        with open(glossary_path, encoding="utf-8") as f:
            loaded: dict = json.load(f)

        return loaded

    def _transcribe_single(self, audio_path: Path, src_lang: str | None = None) -> Path:
        """
        Transcribe a single audio file and return the path to the transcribed JSON.

        If a transcribed JSON file already exists for the given audio, it is reused.

        Args:
            audio_path (Path): Path to the preprocessed audio file.
            src_lang (Optional[str]): Source language. If None, language will be auto-detected.

        Returns:
            Path: Path to the transcribed JSON file.
        """
        transcribed_path = extend_filename(audio_path, "_transcribed").with_suffix(".json")
        if not transcribed_path.exists():
            with Timer("Transcription process"):
                logger.info(
                    f"Audio length: {audio_path}: {format_timestamp(get_audio_duration(audio_path), fmt='srt')}"
                )
                segments, info = self.transcriber.transcribe(audio_path, language=src_lang)
                logger.info(f"Detected language: {info.language}")

            self.to_json(segments, name=transcribed_path, lang=info.language)
        else:
            logger.info(f"Found transcribed json file: {transcribed_path}")
        return transcribed_path

    def produce_transcriptions(self, transcription_queue, audio_paths, src_lang):
        """
        Sequentially produce transcriptions for given audio paths and put them in the queue.

        Args:
            transcription_queue (Queue): Queue to store transcribed paths.
            audio_paths (List[Path]): List of audio file paths to transcribe.
            src_lang (str): Source language for transcription. If None, language will be auto-detected.

        This method processes each audio file sequentially, transcribing it if necessary,
        and puts the path of the transcribed JSON file into the queue.
        """
        for audio_path in audio_paths:
            transcribed_path = self._transcribe_single(audio_path, src_lang)
            transcription_queue.put(transcribed_path)

        transcription_queue.put(None)
        logger.info("Transcription producer finished.")

    def transcribe(
        self,
        paths: str | Path | list[str | Path],
        src_lang: str | None = None,
        noise_suppress: bool = False,
        skip_preprocess: bool = False,
    ) -> list[Path]:
        """
        Transcribe audio/video files and return paths to the transcribed JSON files.

        This method runs only the preprocessing and transcription stages, without
        any translation. It can be used independently when translation is not needed,
        or to produce transcription files that can later be passed to translate().

        Args:
            paths (Union[str, Path, List[Union[str, Path]]]): Audio/Video paths.
            src_lang (Optional[str]): Language of the audio, default to auto-detect.
            noise_suppress (bool): Whether to suppress noise in the audio. Default is False.
            skip_preprocess (bool): Whether to skip the preprocessing step. Default is False.

        Returns:
            List[Path]: List of paths to the transcribed JSON files.
        """
        if not paths:
            logger.warning("No audio/video file given. Skip transcription.")
            return []

        if isinstance(paths, (str, Path)):
            paths = [paths]

        # Keep behavior aligned with pre_process(): de-duplicate repeated inputs.
        paths = [Path(p) for p in set(paths)]

        if skip_preprocess:
            audio_paths = [get_preprocessed_path(p) for p in paths]
            for p in audio_paths:
                if not p.exists():
                    raise FileNotFoundError(
                        f"Preprocessed file not found: {p}. Run pre_process() first or set skip_preprocess=False."
                    )
        else:
            audio_paths = self.pre_process(paths, noise_suppress=noise_suppress)

        logger.info(f"Transcribing {len(audio_paths)} audio files: {pformat(audio_paths)}")

        return [self._transcribe_single(p, src_lang) for p in audio_paths]

    @staticmethod
    def _get_base_name(transcribed_path: Path) -> str:
        """Extract the original audio base name from a transcribed JSON path."""
        return transcribed_path.stem.replace("_preprocessed_transcribed", "")

    def _is_video_transcription(self, transcribed_path: Path, base_name: str) -> bool:
        """
        Determine whether a transcribed file originated from a video source.

        In normal run() flows, self.from_video is populated during preprocessing.
        For standalone translate() usage, infer from an existing source file next to
        the output directory when possible.
        """
        source_stem = transcribed_path.parent.parent / base_name
        if source_stem in self.from_video:
            return True

        video_suffixes = (".mp4", ".mkv", ".mov", ".avi", ".webm", ".ts", ".m4v", ".flv", ".wmv")
        return any(source_stem.with_suffix(suffix).exists() for suffix in video_suffixes)

    def _build_final_subtitle(self, base_name, target_lang, transcribed_opt_sub, skip_trans):
        """
        Build the final subtitle object for a single transcription.

        If skip_trans is True, copies the transcription as the final output.
        Otherwise, performs LLM translation via _translate().

        Args:
            base_name (str): Original audio base name.
            target_lang (str): Target language for translation.
            transcribed_opt_sub (Subtitle): Post-processed transcription subtitle.
            skip_trans (bool): Whether to skip translation.

        Returns:
            Subtitle: The final subtitle (translated or copied), or None on error.
        """
        translated_path = extend_filename(transcribed_opt_sub.filename, "_translated")
        final_json_path = translated_path.with_name(f"{base_name}.json")

        if final_json_path.exists():
            return Subtitle.from_json(final_json_path)

        if skip_trans:
            shutil.copy(transcribed_opt_sub.filename, final_json_path)
            transcribed_opt_sub.filename = final_json_path
            return transcribed_opt_sub

        try:
            with Timer("Translation process"):
                return self._translate(base_name, target_lang, transcribed_opt_sub, translated_path)
        except Exception as e:
            self.exception = e
            return None

    def _generate_subtitle_files(self, subtitle, base_name, subtitle_format):
        """
        Generate subtitle file (.lrc or .srt) and move it to the output directory.

        Args:
            subtitle (Subtitle): The subtitle object to export.
            base_name (str): Original audio base name.
            subtitle_format (str): Output format, either 'lrc' or 'srt'.
        """
        subtitle_path = getattr(subtitle, f"to_{subtitle_format}")()
        result_path = subtitle_path.parent.parent / f"{base_name}.{subtitle_format}"
        shutil.move(subtitle_path, result_path)
        self.transcribed_paths.append(result_path)

    def _handle_bilingual_subtitles(self, transcribed_path, base_name, transcribed_opt_sub, subtitle_format):
        """
        Generate bilingual subtitles and a non-translated subtitle file.

        Args:
            transcribed_path (Path): Path to the transcribed JSON file.
            base_name (str): Original audio base name.
            transcribed_opt_sub (Subtitle): Post-processed transcription subtitle.
            subtitle_format (str): Output format, either 'lrc' or 'srt'.
        """
        bilingual_subtitle = BilingualSubtitle.from_preprocessed(transcribed_path.parent, base_name)
        bilingual_optimizer = SubtitleOptimizer(bilingual_subtitle)
        bilingual_optimizer.extend_time()

        bilingual_path = getattr(bilingual_subtitle, f"to_{subtitle_format}")()
        shutil.move(bilingual_path, bilingual_path.parent.parent / bilingual_path.name)

        non_translated_subtitle = transcribed_opt_sub
        optimizer = SubtitleOptimizer(non_translated_subtitle)
        optimizer.extend_time()
        non_translated_path = getattr(non_translated_subtitle, f"to_{subtitle_format}")()
        shutil.move(non_translated_path, non_translated_path.parent.parent / f"{base_name}_nontrans.{subtitle_format}")

    def _process_transcribed_file(
        self, transcribed_path: Path, target_lang: str | None, skip_trans: bool = False, bilingual_sub: bool = False
    ):
        """
        Process a single transcribed JSON file through post-processing, translation
        (or copy when skip_trans=True), and subtitle generation.

        This is the shared pipeline used by translate(), translation_worker(), and
        the skip_trans branch of run().

        Args:
            transcribed_path (Path): Path to the transcribed JSON file.
            target_lang (Optional[str]): Target language for translation.
            skip_trans (bool): Whether to skip translation.
            bilingual_sub (bool): Whether to generate bilingual subtitles.
        """
        base_name = self._get_base_name(transcribed_path)
        subtitle_format = "srt" if self._is_video_transcription(transcribed_path, base_name) else "lrc"

        transcribed_sub = Subtitle.from_json(transcribed_path)
        transcribed_opt_sub = self.post_process(transcribed_sub, update_name=True)

        final_subtitle = self._build_final_subtitle(base_name, target_lang, transcribed_opt_sub, skip_trans)

        self._generate_subtitle_files(final_subtitle, base_name, subtitle_format)

        if not skip_trans and bilingual_sub:
            self._handle_bilingual_subtitles(transcribed_path, base_name, transcribed_opt_sub, subtitle_format)

    def translate(
        self, transcribed_paths: Path | list[Path], target_lang: str = "zh-cn", bilingual_sub: bool = False
    ) -> list[Path]:
        """
        Translate previously transcribed JSON files and generate subtitle files.

        This method runs only the translation and subtitle generation stages.
        It can be used independently on transcription files produced by transcribe(),
        without needing the original audio files or the whisper model.

        Args:
            transcribed_paths (Union[Path, List[Path]]): Path(s) to transcribed JSON files.
            target_lang (str): Target language for translation. Default is 'zh-cn'.
            bilingual_sub (bool): Whether to generate bilingual subtitles. Default is False.

        Returns:
            List[Path]: List of paths to the generated subtitle files.
        """
        self.transcribed_paths = []
        self.exception = None

        if isinstance(transcribed_paths, Path):
            transcribed_paths = [transcribed_paths]

        logger.info(f"Translating {len(transcribed_paths)} transcribed files: {pformat(transcribed_paths)}")

        for transcribed_path in transcribed_paths:
            self._process_transcribed_file(transcribed_path, target_lang, bilingual_sub=bilingual_sub)

            if self.exception:
                traceback.print_exception(type(self.exception), self.exception, self.exception.__traceback__)
                raise self.exception

        logger.info(f"Total API fee used: {self.api_fee:.4f} USD")

        return self.transcribed_paths

    def consume_transcriptions(self, transcription_queue, target_lang, skip_trans, bilingual_sub):
        """
        Consume transcriptions from the queue using multiple threads for parallel processing.

        Args:
            transcription_queue (Queue): Queue containing paths of transcribed files.
            target_lang (str): Target language for translation.
            skip_trans (bool): Whether to skip the translation process.
            bilingual_sub (bool): Whether to generate bilingual subtitles.

        This method creates multiple worker threads to process transcriptions in parallel,
        handling translation and subtitle generation.
        """
        with concurrent.futures.ThreadPoolExecutor() as executor:
            futures = [
                executor.submit(self.translation_worker, transcription_queue, target_lang, skip_trans, bilingual_sub)
                for _ in range(self.consumer_thread)
            ]
            concurrent.futures.wait(futures)
        logger.info("Transcription consumer finished.")

    def translation_worker(self, transcription_queue, target_lang, skip_trans, bilingual_sub):
        """
        Worker function for parallel translation and subtitle processing.

        Args:
            transcription_queue (Queue): Queue containing paths of transcribed files.
            target_lang (str): Target language for translation.
            skip_trans (bool): Whether to skip the translation process.
            bilingual_sub (bool): Whether to generate bilingual subtitles.

        This method continuously processes transcriptions from the queue, handling translation,
        subtitle generation, and bilingual subtitle creation if required.
        """
        while True:
            logger.debug("Translation worker waiting transcription...")
            transcribed_path = transcription_queue.get()

            if transcribed_path is None:
                transcription_queue.put(None)
                logger.debug("Translation worker finished.")
                return

            logger.info(f"Got transcription: {transcribed_path}")

            self._process_transcribed_file(transcribed_path, target_lang, skip_trans, bilingual_sub)

            logger.info(f"Translation fee til now: {self.api_fee:.4f} USD")

    def _translate(self, audio_name, target_lang, transcribed_opt_sub, translated_path):
        """
        Perform translation of transcribed subtitles.

        Args:
            audio_name (str): Name of the audio file.
            target_lang (str): Target language for translation.
            transcribed_opt_sub (Subtitle): Optimized transcribed subtitle object.
            translated_path (Path): Path to save the translated subtitle.

        Returns:
            Subtitle: Final translated and post-processed subtitle object.

        This method handles the translation process, including context preparation,
        actual translation, and post-processing of the translated subtitles.
        """
        context = TranslateInfo(
            title=audio_name, audio_type="Movie", glossary=self.glossary, forced_glossary=self.is_force_glossary_used
        )

        json_filename = Path(translated_path.parent / (audio_name + ".json"))
        compare_path = Path(translated_path.parent, f"{audio_name}_compare.json")
        if not translated_path.exists():
            # Translate the transcribed json
            translator = LLMTranslator(
                chatbot_model=self.chatbot_model,
                fee_limit=self.fee_limit,
                proxy=self.proxy,
                base_url_config=self.base_url_config,
                retry_model=self.retry_model,
            )

            target_texts = translator.translate(
                transcribed_opt_sub.texts,
                src_lang=transcribed_opt_sub.lang,
                target_lang=target_lang,
                info=context,
                compare_path=compare_path,
            )

            with self._lock:
                self.api_fee += translator.api_fee  # Ensure thread-safe

            translated_sub = deepcopy(transcribed_opt_sub)
            translated_sub.set_texts(target_texts, lang=target_lang)

            # xxx_transcribed_optimized_translated.json
            translated_sub.save(translated_path, update_name=True)
        else:
            logger.info(f"Found translated json file: {translated_path}")
        translated_sub = Subtitle.from_json(translated_path)

        final_subtitle = self.post_process(
            translated_sub, output_name=json_filename, update_name=True, extend_time=True
        )  # xxx.json

        return final_subtitle

    def run(
        self,
        paths: str | Path | list[str | Path],
        src_lang: str | None = None,
        target_lang="zh-cn",
        skip_trans=False,
        noise_suppress=False,
        bilingual_sub=False,
        clear_temp=False,
        skip_preprocess=False,
    ) -> list[str]:
        """
        Run the entire transcription and translation process.

        This method orchestrates the entire workflow of transcribing audio/video files and translating the transcriptions.
        It operates in two parallel phases: transcription (producer) and translation (consumer).

        Detailed process:
        1. Pre-processing:
           - Convert input paths to Path objects.
           - Extract audio from video files if necessary.
           - Apply noise suppression if requested.

        2. Transcription (Producer):
           - Sequentially process each audio file.
           - For each file, either transcribe it or use existing transcription.
           - Put transcribed file paths into a queue.

        3. Translation (Consumer):
           - Create multiple worker threads to process transcriptions in parallel.
           - Each worker:
             a. Retrieves a transcription from the queue.
             b. Translates the transcription (if not skipped).
             c. Generates subtitle files.
             d. Creates bilingual subtitles if requested.

        4. Post-processing:
           - Clear temporary files if requested.

        Args:
            paths (Union[str, Path, List[Union[str, Path]]]): Audio/Video paths, can be a list or a single path.
            src_lang (Optional[str]): Language of the audio, default to auto-detect.
            target_lang (str): Target language for translation, default to Mandarin Chinese ('zh-cn').
            skip_trans (bool): Whether to skip the translation process. Default is False.
            noise_suppress (bool): Whether to suppress noise in the audio. Default is False.
            bilingual_sub (bool): Whether to generate bilingual subtitles. Default is False.
            clear_temp (bool): Whether to clear all temporary files, including generated .wav from video.
                               Set to False to keep intermediate results if errors occur. Default is False.
            skip_preprocess (bool): Whether to skip the preprocessing step. When True, assumes that
                               preprocessed files already exist at the expected locations (as returned by
                               get_preprocessed_path()). This is useful when preprocessing and transcription
                               are run in separate stages. Default is False.

        Returns:
            List[str]: List of paths to the generated subtitle files.

        Raises:
            Exception: If an error occurs during the transcription or translation process.

        Note:
            - The method uses a producer-consumer pattern with a queue to manage parallel processing.
            - It tracks and logs API usage fees for translation services.
            - Temporary files are managed and can be cleared based on the clear_temp parameter.
        """
        self.transcribed_paths = []

        if not paths:
            logger.warning("No audio/video file given. Skip LRCer.run()")
            return []

        if isinstance(paths, (str, Path)):
            paths = [paths]

        paths = list(map(Path, paths))

        if skip_preprocess:
            # Use preprocessed files directly without running preprocessing
            audio_paths = [get_preprocessed_path(p) for p in paths]
            for p in audio_paths:
                if not p.exists():
                    raise FileNotFoundError(
                        f"Preprocessed file not found: {p}. Run pre_process() first or set skip_preprocess=False."
                    )
        else:
            audio_paths = self.pre_process(paths, noise_suppress=noise_suppress)

        if skip_trans:
            # Transcribe-only: no translation threads needed
            transcribed_paths = self.transcribe(paths, src_lang=src_lang, skip_preprocess=True)
            for transcribed_path in transcribed_paths:
                self._process_transcribed_file(transcribed_path, target_lang=None, skip_trans=True)

            if clear_temp:
                logger.info("Clearing temporary folder...")
                self.clear_temp_files(audio_paths)

            return self.transcribed_paths

        logger.info(f"Working on {len(audio_paths)} audio files: {pformat(audio_paths)}")

        transcription_queue = Queue()

        with Timer("Transcription (Producer) and Translation (Consumer) process"):
            consumer = concurrent.futures.ThreadPoolExecutor(thread_name_prefix="Consumer").submit(
                self.consume_transcriptions, transcription_queue, target_lang, skip_trans, bilingual_sub
            )
            producer = concurrent.futures.ThreadPoolExecutor(thread_name_prefix="Producer").submit(
                self.produce_transcriptions, transcription_queue, audio_paths, src_lang
            )

            producer.result()
            consumer.result()

            if self.exception:
                traceback.print_exception(type(self.exception), self.exception, self.exception.__traceback__)
                raise self.exception

        logger.info(f"Total API fee used: {self.api_fee:.4f} USD")

        if clear_temp:
            logger.info("Clearing temporary folder...")
            self.clear_temp_files(audio_paths)

        return self.transcribed_paths

    def clear_temp_files(self, paths):
        """
        Clear the temporary files generated during the transcription and translation process.

        Args:
            paths (List[Path]): List of paths to the processed audio files.

        This method removes temporary folders and generated wave files from video processing.
        """
        temp_folders = {path.parent for path in paths}
        for folder in temp_folders:
            assert folder.name == "preprocessed", f"Not a temporary folder: {folder}"

            shutil.rmtree(folder)
            logger.debug(f"Removed {folder}")

        for input_video_path in self.from_video:
            generated_wave = input_video_path.with_suffix(".wav")
            if generated_wave.exists():
                generated_wave.unlink()
                logger.debug(f"Removed generated wav (from video): {generated_wave}")

    @staticmethod
    def to_json(segments: list[Segment], name, lang):
        """
        Convert transcription segments to JSON format and save to file.

        Args:
            segments (List[Segment]): List of transcription segments.
            name (str): Name of the output JSON file.
            lang (str): Language of the transcription.

        Returns:
            dict: The JSON representation of the transcription.

        This method creates a JSON structure from the transcription segments and saves it to a file.
        """
        result = {"language": lang, "segments": []}

        if not segments:
            result["segments"].append({"start": 0.0, "end": 5.0, "text": "no speech found"})
        else:
            for segment in segments:
                result["segments"].append({"start": segment.start, "end": segment.end, "text": segment.text})

        with open(name, "w", encoding="utf-8") as f:
            json.dump(result, f, ensure_ascii=False, indent=4)

        logger.info(f"File saved to {name}")

        return result

    def pre_process(self, paths, noise_suppress=False):
        """
        Preprocess input audio/video files.

        Args:
            paths (List[Path]): Input file paths
            noise_suppress (bool): Apply noise suppression if True

        Returns:
            List[Path]: Preprocessed audio file paths
        """
        paths = [Path(p) for p in set(paths)]

        for i, path in enumerate(paths):
            if not path.is_file():
                raise FileNotFoundError(f"File not found: {path}")

            if get_file_type(path) == "video":
                self.from_video.add(path.with_suffix(""))
                audio_path = path.with_suffix(".wav")
                if not audio_path.exists():
                    extract_audio(path)
                paths[i] = audio_path

        return Preprocessor(paths, options=self.preprocess_options).run(noise_suppress)

    @staticmethod
    def post_process(
        transcribed_sub: Path | Subtitle | BilingualSubtitle,
        output_name: str | Path | None = None,
        remove_files: list[Path] | None = None,
        update_name: bool = False,
        extend_time: bool = False,
    ):
        """
        Post-process the transcribed subtitles.

        Args:
            transcribed_sub: Path or Subtitle object to post-process.
            output_name: Path for the output file.
            remove_files: List of files to remove after processing.
            update_name: Whether to update the subtitle name.
            extend_time: Whether to extend the time of subtitles.

        Returns:
            Subtitle: The post-processed subtitle object.

        This method applies various optimizations to the transcribed subtitles and saves the result.
        """
        optimizer = SubtitleOptimizer(transcribed_sub)
        optimizer.perform_all(extend_time=extend_time)
        optimizer.save(output_name, update_name=update_name)

        # Remove intermediate files
        if remove_files:
            _ = [file.unlink() for file in remove_files if file.is_file()]

        return optimizer.subtitle
