from __future__ import annotations

import argparse
import asyncio
import copy
import json
import os
import time
import wave
from dataclasses import asdict
from pathlib import Path
from typing import Any

import httpx
from dotenv import load_dotenv

from app.common.call_config import resolve_vad_logging_overrides, resolve_vad_overrides
from app.providers.stt_config import override_config
from app.providers.stt_internal_livekit import (
    InternalHTTPSTT,
    InternalSTTConfig,
    extract_text_from_transcript,
)
from app.tools.local_stresstest.audio import (
    AudioSample,
    audio_metrics,
    pcm_to_wav_bytes,
    vad_proxy_metrics,
    write_wav,
)


# Ajuste estes valores direto no arquivo se quiser rodar sem flags.
DEFAULT_ENV_FILE = ".env.kube.fqa"
DEFAULT_AUDIO_PATH = ""
DEFAULT_OUTPUT_JSON = ""
DEFAULT_SENT_WAV_PATH = ".run/sofya-transcribe/sent_to_sofya.wav"
DEFAULT_SEGMENTS_DIR = ".run/sofya-transcribe/livekit_vad_segments"
DEFAULT_TRANSCRIPTS_OUTPUT = ".run/sofya-transcribe/silero_segments_transcripts.md"
DEFAULT_INPUT_CHANNEL = "cliente"
DEFAULT_CLIENT_CHANNEL = 1
DEFAULT_AGENT_CHANNEL = 2
DEFAULT_USE_LIVEKIT_VAD = True
DEFAULT_VAD_ONLY = False
DEFAULT_VAD_FRAME_MS = 20
DEFAULT_VAD_TAIL_SILENCE_MS = 1600

DEFAULT_CALL_CONFIG = {
    "callConfig": {
        "vad": {
            "minSpeechDuration": 0.15,
            "activationThreshold": 0.30,
            "deactivationThreshold": 0.15,
            "minSilenceDuration": 1.0,
            "prefixPaddingDuration": 1.0,
        },
        "vadLogging": {
            "logDecisions": True,
            "logActivity": True,
            "activityMinProbability": 0.03,
        },
        "ws": {
            "outputGain": 2.0,
        },
    }
}

# VAD do Sofya enviado dentro de override_config.processor.config.extra_params.
# Use None para herdar do env STT_VAD_* ou do override_config padrao.
DEFAULT_SOFYA_VAD_FILTER: bool | None = None
DEFAULT_SOFYA_VAD_THRESHOLD: float | None = None
DEFAULT_SOFYA_VAD_MIN_SILENCE_MS: int | None = None
DEFAULT_SOFYA_VAD_SPEECH_PAD_MS: int | None = None

# Diagnostico local por RMS. Nao substitui o VAD do Sofya, so ajuda a enxergar
# onde o inicio de fala pode estar sendo comido.
DEFAULT_VAD_PROXY_THRESHOLD_DBFS = -45.0
DEFAULT_VAD_PROXY_PREFIX_PADDING_MS: int | None = None
DEFAULT_VAD_PROXY_MIN_SPEECH_MS = 100

# Padding extra antes do WAV enviado ao Sofya, para simular STT_INPUT_PREFIX_PADDING_MS.
DEFAULT_STT_INPUT_PREFIX_PADDING_MS: int | None = None
DEFAULT_STT_INPUT_SUFFIX_PADDING_MS: int | None = None
DEFAULT_STT_PLAYBACK_SPEED = 0.85
DEFAULT_STT_VOLUME_GAIN_DB = 6.0
AUDIO_EXTENSIONS = {".wav", ".mp3", ".flac", ".ogg", ".m4a"}


def _env_str(name: str, default: str = "") -> str:
    value = os.getenv(name)
    return str(default if value is None else value).strip()


def _env_float(name: str, default: float | None = None) -> float | None:
    value = _env_str(name)
    if not value:
        return default
    try:
        return float(value)
    except ValueError:
        return default


def _env_int(name: str, default: int | None = None) -> int | None:
    value = _env_str(name)
    if not value:
        return default
    try:
        return int(value)
    except ValueError:
        return default


def _env_bool(name: str, default: bool | None = None) -> bool | None:
    value = _env_str(name)
    if not value:
        return default
    return value.lower() in {"1", "true", "yes", "y", "on", "sim"}


def _coalesce(*values: Any) -> Any:
    for value in values:
        if value is not None and value != "":
            return value
    return None


def _json_dumps(payload: Any) -> str:
    return json.dumps(payload, ensure_ascii=False, indent=2)


def _float_override(
    value: Any,
    default: float,
    *,
    min_value: float | None = None,
    max_value: float | None = None,
) -> float:
    if value in (None, ""):
        return default
    try:
        resolved = float(value)
    except (TypeError, ValueError):
        return default
    if min_value is not None and resolved < min_value:
        return default
    if max_value is not None and resolved > max_value:
        return default
    return resolved


def _bool_override(value: Any, default: bool) -> bool:
    if value in (None, ""):
        return default
    raw = str(value).strip().lower()
    if raw in {"1", "true", "yes", "on", "sim"}:
        return True
    if raw in {"0", "false", "no", "off", "nao", "não"}:
        return False
    return default


def _default_livekit_vad_config() -> dict[str, float]:
    activation_threshold = float(_env_float("VAD_ACTIVATION_THRESHOLD", 0.35) or 0.35)
    prefix_padding_duration = float(_env_float("VAD_PREFIX_PADDING_DURATION", 1.5) or 1.5)
    prefix_padding_min_duration = float(_env_float("VAD_PREFIX_PADDING_MIN_DURATION", 1.5) or 1.5)
    return {
        "min_speech_duration": float(_env_float("VAD_MIN_SPEECH_DURATION", 0.15) or 0.15),
        "min_silence_duration": float(
            _coalesce(
                _env_float("VAD_MIN_SILENCE_DURATION"),
                _env_float("LIVEKIT_VAD_MIN_SILENCE_DURATION_S"),
                0.35,
            )
        ),
        "activation_threshold": activation_threshold,
        "deactivation_threshold": float(
            _env_float(
                "VAD_DEACTIVATION_THRESHOLD",
                max(activation_threshold - 0.15, 0.01),
            )
            or max(activation_threshold - 0.15, 0.01)
        ),
        "prefix_padding_duration": max(prefix_padding_duration, prefix_padding_min_duration),
    }


def _default_livekit_vad_logging_config() -> dict[str, Any]:
    return {
        "log_decisions": _env_bool("FLOW_LOG_VAD_DECISIONS", False),
        "log_activity": _env_bool("FLOW_LOG_VAD_ACTIVITY", False),
        "activity_min_probability": float(_env_float("FLOW_LOG_VAD_ACTIVITY_MIN_PROB", 0.03) or 0.03),
    }


def _call_config_payload(args: argparse.Namespace) -> dict[str, Any]:
    if args.call_config_json:
        payload = json.loads(args.call_config_json)
    elif args.call_config_file:
        payload = json.loads(Path(args.call_config_file).expanduser().read_text(encoding="utf-8"))
    else:
        payload = copy.deepcopy(DEFAULT_CALL_CONFIG)
    if not isinstance(payload, dict):
        raise ValueError("callConfig precisa ser um objeto JSON")
    call_config = payload.get("callConfig", payload)
    if not isinstance(call_config, dict):
        raise ValueError("callConfig precisa ser um objeto JSON")
    call_config = copy.deepcopy(call_config)
    vad = call_config.setdefault("vad", {})
    if args.lk_min_speech_duration is not None:
        vad["minSpeechDuration"] = args.lk_min_speech_duration
    if args.lk_activation_threshold is not None:
        vad["activationThreshold"] = args.lk_activation_threshold
    if args.lk_deactivation_threshold is not None:
        vad["deactivationThreshold"] = args.lk_deactivation_threshold
    if args.lk_min_silence_duration is not None:
        vad["minSilenceDuration"] = args.lk_min_silence_duration
    if args.lk_prefix_padding_duration is not None:
        vad["prefixPaddingDuration"] = args.lk_prefix_padding_duration

    vad_logging = call_config.setdefault("vadLogging", {})
    if args.lk_log_activity is not None:
        vad_logging["logActivity"] = bool(args.lk_log_activity)
    if args.lk_activity_min_probability is not None:
        vad_logging["activityMinProbability"] = args.lk_activity_min_probability
    return call_config


def _resolve_livekit_vad_config(args: argparse.Namespace) -> tuple[dict[str, float], dict[str, Any], dict[str, Any]]:
    call_config = _call_config_payload(args)
    defaults = _default_livekit_vad_config()
    overrides = resolve_vad_overrides(call_config)
    prefix_padding_min_duration = float(_env_float("VAD_PREFIX_PADDING_MIN_DURATION", 0.0) or 0.0)
    vad_config = {
        "min_speech_duration": _float_override(
            overrides.get("min_speech_duration"),
            defaults["min_speech_duration"],
            min_value=0.0,
        ),
        "min_silence_duration": _float_override(
            overrides.get("min_silence_duration"),
            defaults["min_silence_duration"],
            min_value=0.0,
        ),
        "activation_threshold": _float_override(
            overrides.get("activation_threshold"),
            defaults["activation_threshold"],
            min_value=0.0,
            max_value=1.0,
        ),
        "deactivation_threshold": _float_override(
            overrides.get("deactivation_threshold"),
            defaults["deactivation_threshold"],
            min_value=0.0,
            max_value=1.0,
        ),
        "prefix_padding_duration": _float_override(
            overrides.get("prefix_padding_duration"),
            defaults["prefix_padding_duration"],
            min_value=prefix_padding_min_duration,
        ),
    }

    logging_defaults = _default_livekit_vad_logging_config()
    logging_overrides = resolve_vad_logging_overrides(call_config)
    logging_config = {
        "log_decisions": _bool_override(
            logging_overrides.get("log_decisions"),
            bool(logging_defaults["log_decisions"]),
        ),
        "log_activity": _bool_override(
            logging_overrides.get("log_activity"),
            bool(logging_defaults["log_activity"]),
        ),
        "activity_min_probability": _float_override(
            logging_overrides.get("activity_min_probability"),
            float(logging_defaults["activity_min_probability"]),
            min_value=0.0,
            max_value=1.0,
        ),
    }
    return vad_config, logging_config, call_config


def _build_arg_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Transcreve um arquivo de audio usando o STT Sofya HTTP do projeto.",
    )
    parser.add_argument(
        "--env-file",
        default=DEFAULT_ENV_FILE,
        help=f"Arquivo .env para carregar antes de chamar o Sofya. Default: {DEFAULT_ENV_FILE}",
    )
    parser.add_argument(
        "--override-env",
        action="store_true",
        help="Faz o .env sobrescrever variaveis ja existentes no processo.",
    )
    parser.add_argument(
        "--audio",
        default=DEFAULT_AUDIO_PATH,
        help="Caminho do arquivo ou diretorio de audios. Tambem pode usar SOFYA_AUDIO_FILE.",
    )
    parser.add_argument(
        "--audio-glob",
        default="*.wav",
        help="Padrao usado quando --audio aponta para diretorio. Ex.: '*.wav' ou '*.*'.",
    )
    parser.add_argument(
        "--input-channel",
        default=DEFAULT_INPUT_CHANNEL,
        help=(
            "Canal do WAV usado antes do VAD/STT: mix, cliente/client/left/1 ou "
            "agente/agent/right/2. Default: cliente."
        ),
    )
    mode_group = parser.add_mutually_exclusive_group()
    mode_group.add_argument(
        "--livekit-vad",
        dest="use_livekit_vad",
        action="store_true",
        default=DEFAULT_USE_LIVEKIT_VAD,
        help="Roda o audio pelo Silero VAD do LiveKit antes de chamar o Sofya. Default.",
    )
    mode_group.add_argument(
        "--direct-sofya",
        dest="use_livekit_vad",
        action="store_false",
        help="Chama o Sofya direto com o audio inteiro, sem Silero VAD do LiveKit.",
    )
    parser.add_argument(
        "--vad-only",
        action="store_true",
        default=DEFAULT_VAD_ONLY,
        help="Roda apenas o Silero VAD e salva os segmentos, sem chamar o Sofya.",
    )
    parser.add_argument(
        "--call-config-json",
        default="",
        help="JSON com callConfig para resolver os parametros de VAD do LiveKit.",
    )
    parser.add_argument(
        "--call-config-file",
        default="",
        help="Arquivo JSON com callConfig para resolver os parametros de VAD do LiveKit.",
    )
    parser.add_argument("--url", default="", help="Override de STT_URL.")
    parser.add_argument("--api-key", default="", help="Override de STT_KEY/STT_API_KEY.")
    parser.add_argument("--language", default="", help="Override de STT_LANG.")
    parser.add_argument("--timeout-s", type=float, default=None, help="Timeout HTTP do Sofya.")
    parser.add_argument(
        "--output-mode",
        choices=["threshold_text", "api_text", "raw_json"],
        default="",
        help="Modo de saida do provider. Default vem de STT_OUTPUT_MODE ou threshold_text.",
    )
    parser.add_argument(
        "--min-prob-single-word",
        type=float,
        default=None,
        help="Filtro local para palavra unica no modo threshold_text.",
    )
    parser.add_argument(
        "--initial-prompt",
        default="",
        help="Initial prompt enviado no override_config do Sofya.",
    )

    livekit_vad_group = parser.add_argument_group("Silero VAD do LiveKit")
    livekit_vad_group.add_argument(
        "--lk-min-speech-duration",
        type=float,
        default=None,
        help="callConfig.vad.minSpeechDuration, em segundos.",
    )
    livekit_vad_group.add_argument(
        "--lk-activation-threshold",
        type=float,
        default=None,
        help="callConfig.vad.activationThreshold.",
    )
    livekit_vad_group.add_argument(
        "--lk-deactivation-threshold",
        type=float,
        default=None,
        help="callConfig.vad.deactivationThreshold.",
    )
    livekit_vad_group.add_argument(
        "--lk-min-silence-duration",
        type=float,
        default=None,
        help="callConfig.vad.minSilenceDuration, em segundos.",
    )
    livekit_vad_group.add_argument(
        "--lk-prefix-padding-duration",
        type=float,
        default=None,
        help="callConfig.vad.prefixPaddingDuration, em segundos.",
    )
    livekit_vad_logging = livekit_vad_group.add_mutually_exclusive_group()
    livekit_vad_logging.add_argument(
        "--lk-log-activity",
        dest="lk_log_activity",
        action="store_true",
        default=None,
        help="Habilita logs/metricas de atividade do VAD no resumo.",
    )
    livekit_vad_logging.add_argument(
        "--no-lk-log-activity",
        dest="lk_log_activity",
        action="store_false",
        help="Desabilita logs/metricas de atividade do VAD no resumo.",
    )
    livekit_vad_group.add_argument(
        "--lk-activity-min-probability",
        type=float,
        default=None,
        help="callConfig.vadLogging.activityMinProbability.",
    )
    livekit_vad_group.add_argument(
        "--vad-frame-ms",
        type=int,
        default=DEFAULT_VAD_FRAME_MS,
        help="Tamanho dos frames empurrados para o LiveKit VAD.",
    )
    livekit_vad_group.add_argument(
        "--vad-tail-silence-ms",
        type=int,
        default=DEFAULT_VAD_TAIL_SILENCE_MS,
        help="Silencio ao final para forcar END_OF_SPEECH em audio sem cauda.",
    )
    livekit_vad_group.add_argument(
        "--segments-dir",
        default=DEFAULT_SEGMENTS_DIR,
        help="Diretorio onde os trechos cortados pelo LiveKit VAD serao salvos.",
    )
    livekit_vad_group.add_argument(
        "--transcripts-output",
        default=DEFAULT_TRANSCRIPTS_OUTPUT,
        help="Markdown com segmento Silero, WAV enviado ao Sofya e transcricao por segmento.",
    )

    vad_group = parser.add_argument_group("VAD interno do Sofya, nao e o Silero do LiveKit")
    vad_filter = vad_group.add_mutually_exclusive_group()
    vad_filter.add_argument(
        "--vad-filter",
        dest="vad_filter",
        action="store_true",
        default=DEFAULT_SOFYA_VAD_FILTER,
        help="Envia vad_filter=true no extra_params do Sofya.",
    )
    vad_filter.add_argument(
        "--no-vad-filter",
        dest="vad_filter",
        action="store_false",
        help="Envia vad_filter=false no extra_params do Sofya.",
    )
    vad_group.add_argument(
        "--vad-threshold",
        type=float,
        default=DEFAULT_SOFYA_VAD_THRESHOLD,
        help="threshold em vad_parameters do Sofya. Ex.: 0.72",
    )
    vad_group.add_argument(
        "--vad-min-silence-ms",
        type=int,
        default=DEFAULT_SOFYA_VAD_MIN_SILENCE_MS,
        help="min_silence_duration_ms em vad_parameters do Sofya.",
    )
    vad_group.add_argument(
        "--vad-speech-pad-ms",
        type=int,
        default=DEFAULT_SOFYA_VAD_SPEECH_PAD_MS,
        help="speech_pad_ms em vad_parameters do Sofya.",
    )
    vad_group.add_argument(
        "--stt-input-prefix-padding-ms",
        type=int,
        default=DEFAULT_STT_INPUT_PREFIX_PADDING_MS,
        help="Silencio adicionado antes do WAV enviado ao Sofya.",
    )
    vad_group.add_argument(
        "--stt-input-suffix-padding-ms",
        type=int,
        default=DEFAULT_STT_INPUT_SUFFIX_PADDING_MS,
        help="Silencio adicionado no fim do WAV enviado ao Sofya.",
    )
    vad_group.add_argument(
        "--stt-playback-speed",
        type=float,
        default=None,
        help=(
            "Velocidade do audio enviado ao Sofya. 1.0 mantem original; "
            "0.85 envia mais lento; 1.15 envia mais rapido."
        ),
    )
    vad_group.add_argument(
        "--stt-volume-gain-db",
        type=float,
        default=None,
        help=(
            "Ganho em dB aplicado no audio enviado ao Sofya. "
            "0 mantem original; 6 dobra aproximadamente a amplitude."
        ),
    )

    proxy_group = parser.add_argument_group("Diagnostico VAD proxy local")
    proxy_group.add_argument(
        "--vad-proxy-threshold-dbfs",
        type=float,
        default=DEFAULT_VAD_PROXY_THRESHOLD_DBFS,
        help="Limiar RMS local para diagnostico. Default: -45",
    )
    proxy_group.add_argument(
        "--vad-proxy-prefix-padding-ms",
        type=int,
        default=DEFAULT_VAD_PROXY_PREFIX_PADDING_MS,
        help="Padding local do diagnostico. Default vem de VAD_PREFIX_PADDING_DURATION.",
    )
    proxy_group.add_argument(
        "--vad-proxy-min-speech-ms",
        type=int,
        default=DEFAULT_VAD_PROXY_MIN_SPEECH_MS,
        help="Fala minima usada no diagnostico local.",
    )

    parser.add_argument(
        "--save-sent-wav",
        default=DEFAULT_SENT_WAV_PATH,
        help="Salva o WAV normalizado que foi enviado ao Sofya. Use vazio para desativar.",
    )
    parser.add_argument(
        "--json-output",
        default=DEFAULT_OUTPUT_JSON,
        help="Opcional: salva um resumo JSON da execucao.",
    )
    parser.add_argument(
        "--print-override-config",
        action="store_true",
        help="Imprime o override_config final enviado ao Sofya.",
    )
    return parser


def _audio_path_from_args(args: argparse.Namespace) -> Path:
    raw_path = _coalesce(args.audio, _env_str("SOFYA_AUDIO_FILE"), _env_str("STRESS_AUDIO"))
    if not raw_path:
        raise RuntimeError(
            "Informe o audio com --audio caminho.wav, SOFYA_AUDIO_FILE ou editando DEFAULT_AUDIO_PATH."
        )
    return Path(str(raw_path)).expanduser()


def _resolve_audio_paths(args: argparse.Namespace) -> list[Path]:
    root = _audio_path_from_args(args)
    if root.is_dir():
        paths = [
            path
            for path in sorted(root.glob(str(args.audio_glob or "*.wav")))
            if path.is_file() and path.suffix.lower() in AUDIO_EXTENSIONS
        ]
        if not paths:
            raise RuntimeError(f"Nenhum audio encontrado em {root} com padrao {args.audio_glob!r}.")
        return paths
    return [root]


def _safe_stem(path: Path) -> str:
    return "".join(ch if ch.isalnum() or ch in {"-", "_"} else "_" for ch in path.stem).strip("_") or "audio"


def _channel_spec_from_args(args: argparse.Namespace) -> str:
    return str(_coalesce(args.input_channel, _env_str("SOFYA_INPUT_CHANNEL"), DEFAULT_INPUT_CHANNEL)).strip()


def _resolve_channel_index(channel_spec: str, channels: int) -> int | None:
    spec = (channel_spec or DEFAULT_INPUT_CHANNEL).strip().lower()
    if spec in {"mix", "mixed", "mono", "ambos", "todos", "all"}:
        return None
    if spec in {"cliente", "client", "customer", "left", "l"}:
        channel_number = DEFAULT_CLIENT_CHANNEL if spec in {"cliente", "client", "customer"} else 1
    elif spec in {"agente", "agent", "right", "r"}:
        channel_number = DEFAULT_AGENT_CHANNEL if spec in {"agente", "agent"} else 2
    else:
        try:
            channel_number = int(spec)
        except ValueError as exc:
            raise ValueError(
                f"Canal invalido: {channel_spec}. Use mix, cliente, agente, left, right, 1 ou 2."
            ) from exc

    if channel_number < 1 or channel_number > channels:
        raise ValueError(f"Canal {channel_number} indisponivel: o audio tem {channels} canal(is).")
    return channel_number - 1


def _extract_pcm_channel(pcm: bytes, *, channels: int, channel_index: int) -> bytes:
    if channels == 1:
        return pcm
    sample_width = 2
    frame_width = sample_width * channels
    offset = channel_index * sample_width
    out = bytearray()
    for pos in range(0, len(pcm) - frame_width + 1, frame_width):
        out.extend(pcm[pos + offset : pos + offset + sample_width])
    return bytes(out)


def _mix_pcm_channels(pcm: bytes, *, channels: int) -> bytes:
    import audioop

    if channels == 1:
        return pcm
    if channels == 2:
        return audioop.tomono(pcm, 2, 0.5, 0.5)

    sample_width = 2
    frame_width = sample_width * channels
    out = bytearray()
    for pos in range(0, len(pcm) - frame_width + 1, frame_width):
        total = 0
        for channel_index in range(channels):
            start = pos + (channel_index * sample_width)
            total += int.from_bytes(pcm[start : start + sample_width], byteorder="little", signed=True)
        average = max(-32768, min(32767, round(total / channels)))
        out.extend(int(average).to_bytes(sample_width, byteorder="little", signed=True))
    return bytes(out)


def _resample_mono16(pcm: bytes, *, sample_rate: int, target_sample_rate: int = 16000) -> bytes:
    import audioop

    if sample_rate == target_sample_rate:
        return pcm
    converted, _ = audioop.ratecv(pcm, 2, 1, sample_rate, target_sample_rate, None)
    return converted


def _load_audio(path: Path, *, input_channel: str) -> AudioSample:
    if not path.exists():
        raise FileNotFoundError(f"Audio nao encontrado: {path}")
    try:
        return _load_wav_audio(path, input_channel=input_channel)
    except Exception as wav_error:
        try:
            return _load_with_soundfile(path, input_channel=input_channel)
        except Exception as soundfile_error:
            raise ValueError(
                f"Nao consegui ler {path}. Esperado WAV PCM16, ou formato aceito pelo soundfile. "
                f"wave={wav_error}; soundfile={soundfile_error}"
            ) from soundfile_error


def _load_wav_audio(path: Path, *, input_channel: str) -> AudioSample:
    with wave.open(str(path), "rb") as handle:
        channels = handle.getnchannels()
        sample_width = handle.getsampwidth()
        sample_rate = handle.getframerate()
        pcm = handle.readframes(handle.getnframes())

    if sample_width != 2:
        raise ValueError(f"{path} precisa ser PCM16 para leitura via wave, sample_width={sample_width}")

    channel_index = _resolve_channel_index(input_channel, channels)
    if channel_index is None:
        mono_pcm = _mix_pcm_channels(pcm, channels=channels)
        channel_name = "mix"
    else:
        mono_pcm = _extract_pcm_channel(pcm, channels=channels, channel_index=channel_index)
        channel_name = f"ch{channel_index + 1}"
    mono_pcm = _resample_mono16(mono_pcm, sample_rate=sample_rate)
    return AudioSample(name=f"{path.stem}_{channel_name}", pcm=mono_pcm, sample_rate=16000, channels=1)


def _load_with_soundfile(path: Path, *, input_channel: str) -> AudioSample:
    import audioop

    import soundfile as sf

    data, sample_rate = sf.read(str(path), dtype="int16", always_2d=True)
    if data.size <= 0:
        raise ValueError(f"Audio vazio: {path}")
    channels = int(data.shape[1])
    channel_index = _resolve_channel_index(input_channel, channels)
    if channel_index is None:
        data = data.mean(axis=1).astype("int16").reshape(-1, 1)
        channel_name = "mix"
    else:
        data = data[:, channel_index].astype("int16").reshape(-1, 1)
        channel_name = f"ch{channel_index + 1}"
    pcm = data.tobytes()
    if int(sample_rate) != 16000:
        pcm, _ = audioop.ratecv(pcm, 2, 1, int(sample_rate), 16000, None)
        sample_rate = 16000
    return AudioSample(name=f"{path.stem}_{channel_name}", pcm=pcm, sample_rate=int(sample_rate), channels=1)


def build_sofya_override_config(args: argparse.Namespace) -> dict[str, Any]:
    cfg = copy.deepcopy(override_config)
    processor = cfg.setdefault("processor", {}).setdefault("config", {})
    extra_params = processor.setdefault("extra_params", {})
    vad_params = extra_params.setdefault("vad_parameters", {})

    vad_filter = _coalesce(args.vad_filter, _env_bool("STT_VAD_FILTER"))
    if vad_filter is not None:
        extra_params["vad_filter"] = bool(vad_filter)

    vad_threshold = _coalesce(args.vad_threshold, _env_float("STT_VAD_THRESHOLD"))
    if vad_threshold is not None:
        vad_params["threshold"] = float(vad_threshold)

    vad_min_silence_ms = _coalesce(
        args.vad_min_silence_ms,
        _env_int("STT_VAD_MIN_SILENCE_MS"),
    )
    if vad_min_silence_ms is not None:
        vad_params["min_silence_duration_ms"] = max(0, int(vad_min_silence_ms))

    vad_speech_pad_ms = _coalesce(
        args.vad_speech_pad_ms,
        _env_int("STT_VAD_SPEECH_PAD_MS"),
    )
    if vad_speech_pad_ms is not None:
        vad_params["speech_pad_ms"] = max(0, int(vad_speech_pad_ms))

    prompt = _coalesce(
        args.initial_prompt,
        _env_str("STT_FORCE_INITIAL_PROMPT"),
        _env_str("STT_INITIAL_PROMPT"),
    )
    if prompt:
        extra_params["initial_prompt"] = str(prompt)

    return cfg


def _stt_config(args: argparse.Namespace, config_override: dict[str, Any]) -> InternalSTTConfig:
    url = _coalesce(args.url, _env_str("STT_URL"))
    if not url:
        raise RuntimeError("STT_URL nao configurado. Use --url ou carregue um --env-file com STT_URL.")

    api_key = _coalesce(args.api_key, _env_str("STT_KEY"), _env_str("STT_API_KEY"), "unknow")
    timeout_s = _coalesce(args.timeout_s, _env_float("STT_TIMEOUT_S"), 30.0)
    min_prob_single_word = _coalesce(
        args.min_prob_single_word,
        _env_float("STT_MIN_PROB_SINGLE_WORD"),
        0.10,
    )
    output_mode = _coalesce(args.output_mode, _env_str("STT_OUTPUT_MODE"), "threshold_text")
    return InternalSTTConfig(
        url=str(url),
        api_key=str(api_key),
        language=str(_coalesce(args.language, _env_str("STT_LANG"), "portuguese")),
        timeout_s=float(timeout_s),
        min_prob_single_word=float(min_prob_single_word),
        config_override=json.dumps(config_override, ensure_ascii=False),
        output_mode=str(output_mode),
    )


def _apply_prefix_padding(sample: AudioSample, padding_ms: int) -> tuple[AudioSample, int]:
    padding_ms = max(0, int(padding_ms))
    if padding_ms <= 0:
        return sample, 0
    frames = round(sample.sample_rate * padding_ms / 1000)
    silence = b"\x00" * frames * sample.channels * 2
    if not silence:
        return sample, 0
    actual_ms = round((frames / sample.sample_rate) * 1000)
    return (
        AudioSample(
            name=f"{sample.name}_stt_padded",
            pcm=silence + sample.pcm,
            sample_rate=sample.sample_rate,
            channels=sample.channels,
        ),
        actual_ms,
    )


def _apply_suffix_padding(sample: AudioSample, padding_ms: int) -> tuple[AudioSample, int]:
    padding_ms = max(0, int(padding_ms))
    if padding_ms <= 0:
        return sample, 0
    frames = round(sample.sample_rate * padding_ms / 1000)
    silence = b"\x00" * frames * sample.channels * 2
    if not silence:
        return sample, 0
    actual_ms = round((frames / sample.sample_rate) * 1000)
    return (
        AudioSample(
            name=f"{sample.name}_stt_tail_padded",
            pcm=sample.pcm + silence,
            sample_rate=sample.sample_rate,
            channels=sample.channels,
        ),
        actual_ms,
    )


def _apply_playback_speed(sample: AudioSample, speed: float) -> AudioSample:
    speed = float(speed)
    if speed <= 0:
        raise ValueError("--stt-playback-speed precisa ser maior que zero")
    if abs(speed - 1.0) < 0.0001:
        return sample

    import audioop

    # Resample para outro numero de amostras e mantem o sample_rate original.
    # Ex.: speed=0.85 gera mais amostras e o WAV toca mais lento.
    resample_rate = max(1, round(sample.sample_rate / speed))
    pcm, _ = audioop.ratecv(
        sample.pcm,
        2,
        sample.channels,
        sample.sample_rate,
        resample_rate,
        None,
    )
    speed_name = f"{speed:.3f}".rstrip("0").rstrip(".").replace(".", "p")
    return AudioSample(
        name=f"{sample.name}_speed_{speed_name}x",
        pcm=pcm,
        sample_rate=sample.sample_rate,
        channels=sample.channels,
    )


def _apply_volume_gain_db(sample: AudioSample, gain_db: float) -> AudioSample:
    gain_db = float(gain_db)
    if abs(gain_db) < 0.0001:
        return sample

    import audioop

    factor = 10 ** (gain_db / 20.0)
    pcm = audioop.mul(sample.pcm, 2, factor)
    gain_name = (
        f"{gain_db:+.1f}db"
        .replace("+", "plus")
        .replace("-", "minus")
        .replace(".", "p")
    )
    return AudioSample(
        name=f"{sample.name}_gain_{gain_name}",
        pcm=pcm,
        sample_rate=sample.sample_rate,
        channels=sample.channels,
    )


def _pcm_duration_ms(sample: AudioSample) -> int:
    if not sample.pcm:
        return 0
    frames = len(sample.pcm) // (2 * max(1, sample.channels))
    return round((frames / max(1, sample.sample_rate)) * 1000)


def _tail_silence(sample: AudioSample, duration_ms: int) -> bytes:
    frame_count = round(sample.sample_rate * max(0, duration_ms) / 1000)
    return b"\x00" * frame_count * max(1, sample.channels) * 2


def _iter_rtc_frames(sample: AudioSample, *, frame_ms: int, tail_silence_ms: int):
    from livekit import rtc

    frame_ms = max(1, int(frame_ms))
    samples_per_frame = max(1, round(sample.sample_rate * frame_ms / 1000))
    bytes_per_frame = samples_per_frame * max(1, sample.channels) * 2
    data = sample.pcm + _tail_silence(sample, tail_silence_ms)
    for offset in range(0, len(data), bytes_per_frame):
        chunk = data[offset : offset + bytes_per_frame]
        if not chunk:
            continue
        if len(chunk) < bytes_per_frame:
            chunk += b"\x00" * (bytes_per_frame - len(chunk))
        yield rtc.AudioFrame(
            data=chunk,
            sample_rate=sample.sample_rate,
            num_channels=sample.channels,
            samples_per_channel=len(chunk) // (2 * max(1, sample.channels)),
        )


async def _run_livekit_vad(
    sample: AudioSample,
    *,
    vad_config: dict[str, float],
    logging_config: dict[str, Any],
    frame_ms: int,
    tail_silence_ms: int,
    segments_dir: Path,
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    from livekit.agents import vad as agents_vad
    from livekit.plugins import silero

    segments_dir.mkdir(parents=True, exist_ok=True)
    vad = silero.VAD.load(
        min_speech_duration=vad_config["min_speech_duration"],
        min_silence_duration=vad_config["min_silence_duration"],
        activation_threshold=vad_config["activation_threshold"],
        deactivation_threshold=vad_config["deactivation_threshold"],
        prefix_padding_duration=vad_config["prefix_padding_duration"],
        sample_rate=16000,
    )
    stream = vad.stream()
    segments: list[dict[str, Any]] = []
    diagnostics: dict[str, Any] = {
        "inference_count": 0,
        "max_probability": 0.0,
        "activity_events": [],
        "decision_events": [],
    }

    async def _consume_events() -> None:
        async for ev in stream:
            if ev.type == agents_vad.VADEventType.INFERENCE_DONE:
                probability = float(getattr(ev, "probability", 0.0) or 0.0)
                diagnostics["inference_count"] += 1
                diagnostics["max_probability"] = max(float(diagnostics["max_probability"]), probability)
                if (
                    logging_config["log_activity"]
                    and probability >= logging_config["activity_min_probability"]
                ):
                    diagnostics["activity_events"].append(
                        {
                            "timestamp_ms": round(float(ev.timestamp or 0.0) * 1000),
                            "probability": round(probability, 4),
                            "speaking": bool(getattr(ev, "speaking", False)),
                            "raw_speech_ms": round(float(getattr(ev, "raw_accumulated_speech", 0.0) or 0.0) * 1000),
                            "raw_silence_ms": round(float(getattr(ev, "raw_accumulated_silence", 0.0) or 0.0) * 1000),
                        }
                    )
                continue

            if ev.type == agents_vad.VADEventType.START_OF_SPEECH:
                if logging_config["log_decisions"]:
                    diagnostics["decision_events"].append(
                        {
                            "type": "start_of_speech",
                            "timestamp_ms": round(float(ev.timestamp or 0.0) * 1000),
                            "speech_duration_ms": round(float(ev.speech_duration or 0.0) * 1000),
                        }
                    )
                continue

            if ev.type != agents_vad.VADEventType.END_OF_SPEECH:
                continue

            pcm = b"".join(bytes(frame.data) for frame in ev.frames)
            segment = AudioSample(
                name=f"{sample.name}_segment_{len(segments) + 1:02d}",
                pcm=pcm,
                sample_rate=sample.sample_rate,
                channels=sample.channels,
            )
            duration_ms = _pcm_duration_ms(segment)
            end_ms = round(float(ev.timestamp or 0.0) * 1000)
            start_ms = max(0, end_ms - duration_ms)
            segment_path = write_wav(
                segments_dir / f"{len(segments) + 1:02d}_{segment.name}.wav",
                segment,
            )
            metrics = audio_metrics(segment.pcm, sample_rate=segment.sample_rate, channels=segment.channels)
            row = {
                "index": len(segments) + 1,
                "path": str(segment_path),
                "start_ms": start_ms,
                "end_ms": end_ms,
                "duration_ms": duration_ms,
                "speech_duration_ms": round(float(ev.speech_duration or 0.0) * 1000),
                "silence_duration_ms": round(float(ev.silence_duration or 0.0) * 1000),
                "rms_dbfs": metrics.rms_dbfs,
                "peak_dbfs": metrics.peak_dbfs,
                "pcm": segment.pcm,
            }
            segments.append(row)
            if logging_config["log_decisions"]:
                diagnostics["decision_events"].append(
                    {
                        "type": "end_of_speech",
                        "timestamp_ms": end_ms,
                        "speech_duration_ms": row["speech_duration_ms"],
                        "silence_duration_ms": row["silence_duration_ms"],
                        "segment_duration_ms": duration_ms,
                    }
                )

    consumer = asyncio.create_task(_consume_events())
    try:
        for frame in _iter_rtc_frames(
            sample,
            frame_ms=frame_ms,
            tail_silence_ms=tail_silence_ms,
        ):
            stream.push_frame(frame)
            await asyncio.sleep(0)
        stream.end_input()
        await consumer
    finally:
        if not consumer.done():
            consumer.cancel()
            await asyncio.gather(consumer, return_exceptions=True)
        try:
            await stream.aclose()
        except Exception:
            pass

    return segments, diagnostics


def _resolve_vad_proxy_prefix_padding_ms(args: argparse.Namespace) -> int:
    explicit = _coalesce(
        args.vad_proxy_prefix_padding_ms,
        _env_int("STRESS_VAD_PREFIX_PADDING_MS"),
    )
    if explicit is not None:
        return max(0, int(explicit))
    return max(0, round(float(_env_float("VAD_PREFIX_PADDING_DURATION", 1.0) or 1.0) * 1000))


def _resolve_stt_input_padding_ms(args: argparse.Namespace) -> int:
    value = _coalesce(
        args.stt_input_prefix_padding_ms,
        _env_int("STT_INPUT_PREFIX_PADDING_MS"),
        250,
    )
    return max(0, int(value))


def _resolve_stt_input_suffix_padding_ms(args: argparse.Namespace) -> int:
    value = _coalesce(
        args.stt_input_suffix_padding_ms,
        _env_int("STT_INPUT_SUFFIX_PADDING_MS"),
        0,
    )
    return max(0, int(value))


def _resolve_stt_playback_speed(args: argparse.Namespace) -> float:
    value = _coalesce(
        args.stt_playback_speed,
        _env_float("STT_PLAYBACK_SPEED"),
        DEFAULT_STT_PLAYBACK_SPEED,
    )
    speed = float(value)
    if speed <= 0:
        raise ValueError("--stt-playback-speed precisa ser maior que zero")
    return speed


def _resolve_stt_volume_gain_db(args: argparse.Namespace) -> float:
    value = _coalesce(
        args.stt_volume_gain_db,
        _env_float("STT_VOLUME_GAIN_DB"),
        DEFAULT_STT_VOLUME_GAIN_DB,
    )
    return float(value)


def _result_payload(
    *,
    audio_path: Path,
    input_channel: str,
    sent_wav_path: Path | None,
    cfg: InternalSTTConfig,
    override: dict[str, Any],
    source_sample: AudioSample,
    sent_sample: AudioSample,
    applied_padding_ms: int,
    applied_suffix_padding_ms: int,
    stt_playback_speed: float,
    stt_volume_gain_db: float,
    vad_proxy: Any,
    transcript_raw: str,
    latency_ms: int,
) -> dict[str, Any]:
    return {
        "audio_path": str(audio_path),
        "input_channel": input_channel,
        "sent_wav_path": str(sent_wav_path) if sent_wav_path is not None else "",
        "stt_url": cfg.url,
        "language": cfg.language,
        "output_mode": cfg.output_mode,
        "latency_ms": latency_ms,
        "source_audio": asdict(audio_metrics(source_sample.pcm, sample_rate=source_sample.sample_rate)),
        "sent_audio": asdict(audio_metrics(sent_sample.pcm, sample_rate=sent_sample.sample_rate)),
        "applied_stt_input_prefix_padding_ms": applied_padding_ms,
        "applied_stt_input_suffix_padding_ms": applied_suffix_padding_ms,
        "stt_playback_speed": stt_playback_speed,
        "stt_volume_gain_db": stt_volume_gain_db,
        "vad_proxy": asdict(vad_proxy),
        "sofya_vad": override["processor"]["config"]["extra_params"].get("vad_parameters", {}),
        "sofya_vad_filter": override["processor"]["config"]["extra_params"].get("vad_filter"),
        "transcript": extract_text_from_transcript(transcript_raw),
        "transcript_raw": transcript_raw,
    }


async def _post_sample_to_sofya(
    stt: InternalHTTPSTT,
    *,
    sample: AudioSample,
    req_id: str,
    language: str,
) -> dict[str, Any]:
    metrics = audio_metrics(sample.pcm, sample_rate=sample.sample_rate, channels=sample.channels)
    started = time.perf_counter()
    event = await stt._post_internal_http(
        sample.pcm,
        pcm_to_wav_bytes(sample),
        req_id=req_id,
        started_ns=time.time_ns(),
        language=language,
        audio_duration_ms=metrics.duration_ms,
        level_dbfs=metrics.rms_dbfs,
    )
    latency_ms = round((time.perf_counter() - started) * 1000)
    transcript_raw = event.alternatives[0].text if event.alternatives else ""
    return {
        "latency_ms": latency_ms,
        "transcript_raw": transcript_raw,
        "transcript": extract_text_from_transcript(transcript_raw),
    }


def _print_livekit_vad_summary(
    *,
    vad_config: dict[str, float],
    logging_config: dict[str, Any],
    segments: list[dict[str, Any]],
    diagnostics: dict[str, Any],
) -> None:
    print("[sofya-transcribe] livekit_vad_config:")
    print(_json_dumps(vad_config))
    print("[sofya-transcribe] livekit_vad_logging:")
    print(_json_dumps(logging_config))
    print(
        "[sofya-transcribe] livekit_vad "
        f"segments={len(segments)} "
        f"inference_count={diagnostics['inference_count']} "
        f"max_probability={float(diagnostics['max_probability']):.4f}"
    )
    for row in segments:
        print(
            "[sofya-transcribe] segment "
            f"#{row['index']} start_ms={row['start_ms']} end_ms={row['end_ms']} "
            f"duration_ms={row['duration_ms']} speech_ms={row['speech_duration_ms']} "
            f"silence_ms={row['silence_duration_ms']} rms_dbfs={row['rms_dbfs']} "
            f"path={row['path']}"
        )


def _format_ms(value: Any) -> str:
    try:
        total_ms = max(0, int(round(float(value))))
    except (TypeError, ValueError):
        total_ms = 0
    minutes, rest = divmod(total_ms, 60_000)
    seconds, ms = divmod(rest, 1000)
    return f"{minutes:02d}:{seconds:02d}.{ms:03d}"


def _write_segment_transcripts(
    path: Path,
    *,
    audio_path: Path,
    input_channel: str,
    vad_config: dict[str, float],
    segments: list[dict[str, Any]],
    transcript: str,
) -> Path:
    path.parent.mkdir(parents=True, exist_ok=True)
    lines = [
        "# Silero VAD -> Sofya STT",
        "",
        f"- Audio original: `{audio_path}`",
        f"- Canal usado: `{input_channel}`",
        "- Observacao: Silero nao transcreve texto; ele corta audio. Os textos abaixo sao retornos do Sofya para cada segmento cortado pelo Silero.",
        "- VAD LiveKit:",
        f"  - min_speech_duration: `{vad_config['min_speech_duration']}`",
        f"  - activation_threshold: `{vad_config['activation_threshold']}`",
        f"  - deactivation_threshold: `{vad_config['deactivation_threshold']}`",
        f"  - min_silence_duration: `{vad_config['min_silence_duration']}`",
        f"  - prefix_padding_duration: `{vad_config['prefix_padding_duration']}`",
        "",
        "## Transcricao Final",
        "",
        transcript or "_Sem texto transcrito._",
        "",
        "## Segmentos",
        "",
    ]
    for row in segments:
        text = str(row.get("transcript") or "").strip()
        lines.extend(
            [
                f"### Segmento {int(row['index']):02d}",
                "",
                f"- Intervalo original: `{_format_ms(row.get('start_ms'))}` -> `{_format_ms(row.get('end_ms'))}`",
                f"- Duracao enviada: `{row.get('duration_ms', '')} ms`",
                f"- Fala estimada pelo VAD: `{row.get('speech_duration_ms', '')} ms`",
                f"- Silencio de fechamento: `{row.get('silence_duration_ms', '')} ms`",
                f"- RMS: `{row.get('rms_dbfs', '')} dBFS`",
                f"- WAV cortado pelo Silero: `{row.get('path', '')}`",
                f"- WAV enviado ao Sofya: `{row.get('sent_to_sofya_path', '')}`",
                f"- Padding inicio enviado ao Sofya: `{row.get('applied_stt_input_prefix_padding_ms', '')} ms`",
                f"- Padding fim enviado ao Sofya: `{row.get('applied_stt_input_suffix_padding_ms', '')} ms`",
                f"- Velocidade enviada ao Sofya: `{row.get('stt_playback_speed', '')}`",
                f"- Ganho enviado ao Sofya: `{row.get('stt_volume_gain_db', '')} dB`",
                "",
                "Transcricao:",
                "",
                text or "_Sem texto._",
                "",
            ]
        )
    path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
    return path


def _write_batch_transcripts(path: Path, results: list[dict[str, Any]]) -> Path:
    path.parent.mkdir(parents=True, exist_ok=True)
    lines = [
        "# Batch Silero VAD -> Sofya STT",
        "",
        f"- Audios processados: `{len(results)}`",
        "- Observacao: Silero corta audio; os textos sao retornos do Sofya para cada segmento enviado.",
        "",
        "## Resumo",
        "",
        "| audio | canal | segmentos | transcricao |",
        "| --- | --- | ---: | --- |",
    ]
    for result in results:
        transcript = str(result.get("transcript") or "").replace("\n", " ").strip()
        if len(transcript) > 160:
            transcript = transcript[:157].rstrip() + "..."
        lines.append(
            f"| `{result.get('audio_path', '')}` | `{result.get('input_channel', '')}` | "
            f"{len(result.get('segments') or [])} | {transcript or '_Sem texto_'} |"
        )

    for audio_idx, result in enumerate(results, start=1):
        lines.extend(
            [
                "",
                f"## Audio {audio_idx:02d}: `{result.get('audio_path', '')}`",
                "",
                f"- Canal usado: `{result.get('input_channel', '')}`",
                f"- Segmentos: `{len(result.get('segments') or [])}`",
                f"- Markdown individual: `{result.get('transcripts_output', '')}`",
                "",
                "Transcricao final:",
                "",
                str(result.get("transcript") or "").strip() or "_Sem texto transcrito._",
                "",
                "### Segmentos",
                "",
            ]
        )
        for row in result.get("segments") or []:
            text = str(row.get("transcript") or "").strip()
            lines.extend(
                [
                    f"#### Segmento {int(row['index']):02d}",
                    "",
                    f"- Intervalo original: `{_format_ms(row.get('start_ms'))}` -> `{_format_ms(row.get('end_ms'))}`",
                    f"- Duracao enviada: `{row.get('duration_ms', '')} ms`",
                    f"- Fala estimada pelo VAD: `{row.get('speech_duration_ms', '')} ms`",
                    f"- Silencio de fechamento: `{row.get('silence_duration_ms', '')} ms`",
                    f"- RMS: `{row.get('rms_dbfs', '')} dBFS`",
                    f"- WAV cortado pelo Silero: `{row.get('path', '')}`",
                    f"- WAV enviado ao Sofya: `{row.get('sent_to_sofya_path', '')}`",
                    f"- Padding inicio enviado ao Sofya: `{row.get('applied_stt_input_prefix_padding_ms', '')} ms`",
                    f"- Padding fim enviado ao Sofya: `{row.get('applied_stt_input_suffix_padding_ms', '')} ms`",
                    f"- Velocidade enviada ao Sofya: `{row.get('stt_playback_speed', '')}`",
                    f"- Ganho enviado ao Sofya: `{row.get('stt_volume_gain_db', '')} dB`",
                    "",
                    "Transcricao:",
                    "",
                    text or "_Sem texto._",
                    "",
                ]
            )
    path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
    return path


async def _run_one_audio(
    args: argparse.Namespace,
    *,
    audio_path: Path,
    input_channel: str,
    segments_dir: Path | None = None,
    transcripts_output: Path | None = None,
    json_output: Path | None = None,
) -> dict[str, Any]:
    source_sample = _load_audio(audio_path, input_channel=input_channel)
    source_metrics = audio_metrics(
        source_sample.pcm,
        sample_rate=source_sample.sample_rate,
        channels=source_sample.channels,
    )
    vad_proxy = vad_proxy_metrics(
        source_sample,
        threshold_dbfs=float(args.vad_proxy_threshold_dbfs),
        prefix_padding_ms=_resolve_vad_proxy_prefix_padding_ms(args),
        min_speech_ms=max(0, int(args.vad_proxy_min_speech_ms)),
    )

    print(
        "[sofya-transcribe] audio="
        f"{audio_path} input_channel={input_channel} normalized_sample={source_sample.name} "
        f"duration_ms={source_metrics.duration_ms} rms_dbfs={source_metrics.rms_dbfs}"
    )
    print(
        "[sofya-transcribe] vad_proxy "
        f"first_voice_ms={vad_proxy.first_voice_ms} "
        f"speech_ms={vad_proxy.speech_ms} "
        f"low_start_risk={vad_proxy.low_start_risk}"
    )
    stt_playback_speed = _resolve_stt_playback_speed(args)
    stt_volume_gain_db = _resolve_stt_volume_gain_db(args)
    print(f"[sofya-transcribe] stt_playback_speed={stt_playback_speed}")
    print(f"[sofya-transcribe] stt_volume_gain_db={stt_volume_gain_db}")

    if args.use_livekit_vad:
        vad_config, vad_logging_config, call_config = _resolve_livekit_vad_config(args)
        segments_dir = segments_dir or Path(args.segments_dir).expanduser()
        segments, diagnostics = await _run_livekit_vad(
            source_sample,
            vad_config=vad_config,
            logging_config=vad_logging_config,
            frame_ms=max(1, int(args.vad_frame_ms)),
            tail_silence_ms=max(0, int(args.vad_tail_silence_ms)),
            segments_dir=segments_dir,
        )
        _print_livekit_vad_summary(
            vad_config=vad_config,
            logging_config=vad_logging_config,
            segments=segments,
            diagnostics=diagnostics,
        )

        if args.vad_only:
            result = {
                "mode": "livekit_vad_only",
                "audio_path": str(audio_path),
                "input_channel": input_channel,
                "source_audio": asdict(source_metrics),
                "vad_proxy": asdict(vad_proxy),
                "call_config": call_config,
                "livekit_vad": vad_config,
                "livekit_vad_logging": vad_logging_config,
                "livekit_vad_diagnostics": diagnostics,
                "segments": [{key: value for key, value in row.items() if key != "pcm"} for row in segments],
            }
            if json_output is not None:
                json_path = json_output
                json_path.parent.mkdir(parents=True, exist_ok=True)
                json_path.write_text(_json_dumps(result) + "\n", encoding="utf-8")
                print(f"[sofya-transcribe] json_output={json_path}")
            print("[sofya-transcribe] vad_only=true; Sofya nao foi chamado.")
            return result

        config_override = build_sofya_override_config(args)
        cfg = _stt_config(args, config_override)
        os.environ.pop("STT_FORCE_OVERRIDE_CONFIG", None)
        if args.print_override_config:
            print("[sofya-transcribe] override_config enviado ao Sofya:")
            print(_json_dumps(config_override))
        print(
            "[sofya-transcribe] sofya_vad "
            f"filter={config_override['processor']['config']['extra_params'].get('vad_filter')} "
            f"params={config_override['processor']['config']['extra_params'].get('vad_parameters')}"
        )

        stt_input_padding_ms = _resolve_stt_input_padding_ms(args)
        stt_input_suffix_padding_ms = _resolve_stt_input_suffix_padding_ms(args)
        async with httpx.AsyncClient() as client:
            stt = InternalHTTPSTT(cfg, client=client)
            for row in segments:
                segment_sample = AudioSample(
                    name=f"{source_sample.name}_segment_{row['index']:02d}",
                    pcm=row["pcm"],
                    sample_rate=source_sample.sample_rate,
                    channels=source_sample.channels,
                )
                speed_adjusted_segment = _apply_playback_speed(segment_sample, stt_playback_speed)
                gain_adjusted_segment = _apply_volume_gain_db(
                    speed_adjusted_segment,
                    stt_volume_gain_db,
                )
                sent_segment, applied_padding_ms = _apply_prefix_padding(
                    gain_adjusted_segment,
                    stt_input_padding_ms,
                )
                sent_segment, applied_suffix_padding_ms = _apply_suffix_padding(
                    sent_segment,
                    stt_input_suffix_padding_ms,
                )
                sent_path = write_wav(
                    segments_dir / f"{row['index']:02d}_{sent_segment.name}_sent_to_sofya.wav",
                    sent_segment,
                )
                stt_result = await _post_sample_to_sofya(
                    stt,
                    sample=sent_segment,
                    req_id=f"sofya-file-{audio_path.stem}-seg-{row['index']:02d}",
                    language=cfg.language,
                )
                row.update(
                    {
                        "sent_to_sofya_path": str(sent_path),
                        "applied_stt_input_prefix_padding_ms": applied_padding_ms,
                        "applied_stt_input_suffix_padding_ms": applied_suffix_padding_ms,
                        "stt_playback_speed": stt_playback_speed,
                        "stt_volume_gain_db": stt_volume_gain_db,
                        "sofya_latency_ms": stt_result["latency_ms"],
                        "transcript": stt_result["transcript"],
                        "transcript_raw": stt_result["transcript_raw"],
                    }
                )
                print(
                    "[sofya-transcribe] transcript "
                    f"segment=#{row['index']} latency_ms={row['sofya_latency_ms']} "
                    f"text={row['transcript']}"
                )

        transcript = " ".join(str(row.get("transcript") or "").strip() for row in segments).strip()
        transcripts_output = transcripts_output if transcripts_output is not None else (
            Path(args.transcripts_output).expanduser() if args.transcripts_output else None
        )
        if transcripts_output is not None:
            transcripts_path = _write_segment_transcripts(
                transcripts_output,
                audio_path=audio_path,
                input_channel=input_channel,
                vad_config=vad_config,
                segments=segments,
                transcript=transcript,
            )
            print(f"[sofya-transcribe] transcripts_output={transcripts_path}")
        result = {
            "mode": "livekit_vad_to_sofya",
            "audio_path": str(audio_path),
            "input_channel": input_channel,
            "source_audio": asdict(source_metrics),
            "vad_proxy": asdict(vad_proxy),
            "call_config": call_config,
            "livekit_vad": vad_config,
            "livekit_vad_logging": vad_logging_config,
            "livekit_vad_diagnostics": diagnostics,
            "sofya_vad": config_override["processor"]["config"]["extra_params"].get("vad_parameters", {}),
            "sofya_vad_filter": config_override["processor"]["config"]["extra_params"].get("vad_filter"),
            "stt_input_prefix_padding_ms": stt_input_padding_ms,
            "stt_input_suffix_padding_ms": stt_input_suffix_padding_ms,
            "stt_playback_speed": stt_playback_speed,
            "stt_volume_gain_db": stt_volume_gain_db,
            "transcript": transcript,
            "transcripts_output": str(transcripts_output) if transcripts_output is not None else "",
            "segments": [{key: value for key, value in row.items() if key != "pcm"} for row in segments],
        }
        if json_output is not None:
            json_path = json_output
            json_path.parent.mkdir(parents=True, exist_ok=True)
            json_path.write_text(_json_dumps(result) + "\n", encoding="utf-8")
            print(f"[sofya-transcribe] json_output={json_path}")
        print("[sofya-transcribe] transcript_final:")
        print(transcript)
        return result

    config_override = build_sofya_override_config(args)
    cfg = _stt_config(args, config_override)

    # Garante que os parametros montados por este script ganhem de um
    # STT_FORCE_OVERRIDE_CONFIG herdado do shell.
    os.environ.pop("STT_FORCE_OVERRIDE_CONFIG", None)

    speed_adjusted_sample = _apply_playback_speed(source_sample, stt_playback_speed)
    gain_adjusted_sample = _apply_volume_gain_db(speed_adjusted_sample, stt_volume_gain_db)
    sent_sample, applied_padding_ms = _apply_prefix_padding(
        gain_adjusted_sample,
        _resolve_stt_input_padding_ms(args),
    )
    sent_sample, applied_suffix_padding_ms = _apply_suffix_padding(
        sent_sample,
        _resolve_stt_input_suffix_padding_ms(args),
    )
    sent_wav_path = Path(args.save_sent_wav).expanduser() if args.save_sent_wav else None
    if sent_wav_path is not None:
        write_wav(sent_wav_path, sent_sample)

    if args.print_override_config:
        print("[sofya-transcribe] override_config enviado ao Sofya:")
        print(_json_dumps(config_override))

    print(
        "[sofya-transcribe] sofya_vad "
        f"filter={config_override['processor']['config']['extra_params'].get('vad_filter')} "
        f"params={config_override['processor']['config']['extra_params'].get('vad_parameters')}"
    )

    async with httpx.AsyncClient() as client:
        stt = InternalHTTPSTT(cfg, client=client)
        stt_result = await _post_sample_to_sofya(
            stt,
            sample=sent_sample,
            req_id=f"sofya-file-{audio_path.stem}",
            language=cfg.language,
        )
    latency_ms = stt_result["latency_ms"]
    transcript_raw = stt_result["transcript_raw"]
    transcript = extract_text_from_transcript(transcript_raw)

    result = _result_payload(
        audio_path=audio_path,
        input_channel=input_channel,
        sent_wav_path=sent_wav_path,
        cfg=cfg,
        override=config_override,
        source_sample=source_sample,
        sent_sample=sent_sample,
        applied_padding_ms=applied_padding_ms,
        applied_suffix_padding_ms=applied_suffix_padding_ms,
        stt_playback_speed=stt_playback_speed,
        stt_volume_gain_db=stt_volume_gain_db,
        vad_proxy=vad_proxy,
        transcript_raw=transcript_raw,
        latency_ms=latency_ms,
    )
    if json_output is not None:
        json_path = json_output
        json_path.parent.mkdir(parents=True, exist_ok=True)
        json_path.write_text(_json_dumps(result) + "\n", encoding="utf-8")
        print(f"[sofya-transcribe] json_output={json_path}")

    print(f"[sofya-transcribe] latency_ms={latency_ms}")
    print("[sofya-transcribe] transcript:")
    print(transcript)
    if cfg.output_mode == "raw_json":
        print("[sofya-transcribe] transcript_raw:")
        print(transcript_raw)
    return result


async def run(args: argparse.Namespace) -> int:
    env_file = Path(args.env_file).expanduser() if args.env_file else None
    if env_file and env_file.exists():
        load_dotenv(env_file, override=bool(args.override_env))

    input_channel = _channel_spec_from_args(args)
    audio_paths = _resolve_audio_paths(args)
    base_segments_dir = Path(args.segments_dir).expanduser()
    base_transcripts_output = Path(args.transcripts_output).expanduser() if args.transcripts_output else None
    base_json_output = Path(args.json_output).expanduser() if args.json_output else None

    if len(audio_paths) == 1:
        await _run_one_audio(
            args,
            audio_path=audio_paths[0],
            input_channel=input_channel,
            segments_dir=base_segments_dir,
            transcripts_output=base_transcripts_output,
            json_output=base_json_output,
        )
        return 0

    print(f"[sofya-transcribe] batch audios={len(audio_paths)}")
    results: list[dict[str, Any]] = []
    individual_dir = (
        base_transcripts_output.parent / f"{base_transcripts_output.stem}_files"
        if base_transcripts_output is not None
        else base_segments_dir / "reports"
    )
    for idx, audio_path in enumerate(audio_paths, start=1):
        safe_name = f"{idx:02d}_{_safe_stem(audio_path)}"
        print(f"[sofya-transcribe] batch_item {idx}/{len(audio_paths)} audio={audio_path}")
        result = await _run_one_audio(
            args,
            audio_path=audio_path,
            input_channel=input_channel,
            segments_dir=base_segments_dir / safe_name,
            transcripts_output=individual_dir / f"{safe_name}_segments_transcripts.md",
            json_output=None,
        )
        results.append(result)

    if base_transcripts_output is not None:
        batch_path = _write_batch_transcripts(base_transcripts_output, results)
        print(f"[sofya-transcribe] batch_transcripts_output={batch_path}")

    if base_json_output is not None:
        payload = {
            "mode": "batch",
            "input_channel": input_channel,
            "audio_count": len(audio_paths),
            "transcripts_output": str(base_transcripts_output) if base_transcripts_output is not None else "",
            "results": results,
        }
        base_json_output.parent.mkdir(parents=True, exist_ok=True)
        base_json_output.write_text(_json_dumps(payload) + "\n", encoding="utf-8")
        print(f"[sofya-transcribe] batch_json_output={base_json_output}")

    return 0


def main() -> None:
    parser = _build_arg_parser()
    args = parser.parse_args()
    raise SystemExit(asyncio.run(run(args)))


if __name__ == "__main__":
    main()
