package com.aletheia.app.ui.reader.tts

import ai.onnxruntime.OnnxTensor
import ai.onnxruntime.OrtEnvironment
import ai.onnxruntime.OrtSession
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioTrack
import android.media.PlaybackParams
import android.util.Log
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.RandomAccessFile
import java.nio.FloatBuffer
import java.nio.LongBuffer
import java.nio.charset.StandardCharsets
import java.util.LinkedHashMap
import java.util.Locale
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import kotlin.coroutines.coroutineContext

class VoskSpeechEngine(context: Context) {
    private val appContext = context.applicationContext
    private val playbackGeneration = AtomicInteger()
    private val trackLock = Any()
    private val environment = OrtEnvironment.getEnvironment()
    private var synthesisSession: OrtSession? = null
    private var bertSession: OrtSession? = null
    private var dictionary: PronunciationDictionary? = null
    private var tokenizer: WordPieceTokenizer? = null
    private var activeTrack: AudioTrack? = null

    suspend fun initialize() = withContext(Dispatchers.IO) {
        if (synthesisSession != null) return@withContext
        val modelFile = copyAssetToInternalStorage(MODEL_ASSET, MODEL_FILE)
        val bertFile = copyAssetToInternalStorage(BERT_ASSET, BERT_FILE)
        val dictionaryFile = copyAssetToInternalStorage(DICTIONARY_ASSET, DICTIONARY_FILE)
        val dictionaryIndex = appContext.assets.open(DICTIONARY_INDEX_ASSET).bufferedReader().useLines { lines ->
            lines.mapNotNull { line ->
                val separator = line.lastIndexOf('\t')
                if (separator <= 0) null else DictionaryIndexEntry(
                    line.substring(0, separator),
                    line.substring(separator + 1).toLong()
                )
            }.toList()
        }
        val vocabulary = appContext.assets.open(VOCABULARY_ASSET).bufferedReader().useLines { lines ->
            lines.mapIndexed { index, token -> token to index.toLong() }.toMap()
        }
        val options = OrtSession.SessionOptions().apply {
            setOptimizationLevel(OrtSession.SessionOptions.OptLevel.NO_OPT)
            setIntraOpNumThreads(INFERENCE_THREADS)
            setInterOpNumThreads(1)
        }
        synthesisSession = environment.createSession(modelFile.absolutePath, options)
        bertSession = environment.createSession(bertFile.absolutePath, options)
        dictionary = PronunciationDictionary(dictionaryFile, dictionaryIndex)
        tokenizer = WordPieceTokenizer(vocabulary)
        Log.i(
            TAG,
            "Vosk TTS 0.9 loaded: model=${modelFile.length()} bert=${bertFile.length()} " +
                "dictionary=${dictionaryFile.length()}"
        )
    }

    suspend fun speak(
        text: String,
        rate: Float,
        pitch: Float,
        pauseScale: Float,
        articulationScale: Float
    ) = withContext(Dispatchers.Default) {
        val localSynthesis = checkNotNull(synthesisSession) { "Vosk TTS is not initialized" }
        val localBert = checkNotNull(bertSession) { "Vosk BERT is not initialized" }
        val localDictionary = checkNotNull(dictionary) { "Vosk dictionary is not initialized" }
        val localTokenizer = checkNotNull(tokenizer) { "Vosk tokenizer is not initialized" }
        val generation = playbackGeneration.incrementAndGet()
        for (chunk in splitText(text)) {
            coroutineContext.ensureActive()
            check(generation == playbackGeneration.get()) { "Playback stopped" }
            val prepared = prepareInput(chunk.text, localDictionary, localTokenizer, localBert)
            if (prepared.timeSteps <= 2) continue
            Log.d(TAG, "Synthesizing ${chunk.text.length} chars as ${prepared.timeSteps} phonemes")
            val output = synthesize(localSynthesis, prepared, articulationScale)
            check(output.isNotEmpty()) { "Vosk returned empty audio" }
            val playbackSpeed = (rate * BOOK_SPEED_SCALE).coerceIn(MIN_PLAYBACK_SPEED, MAX_PLAYBACK_SPEED)
            val scaledPauseMs = if (chunk.trailingPauseMs == NO_TRAILING_PAUSE_MS) {
                NO_TRAILING_PAUSE_MS
            } else {
                (chunk.trailingPauseMs * pauseScale / rate.coerceAtLeast(MIN_RATE_FOR_PAUSE_SCALING))
                    .toInt()
                    .coerceAtLeast(MIN_TRAILING_PAUSE_MS)
                    .coerceAtMost(chunk.maximumPauseMs)
            }
            Log.d(
                TAG,
                "Generated ${output.size} samples at $SAMPLE_RATE Hz for ${chunk.text.length} chars; " +
                    "speaker=$MALE_1_SPEAKER_ID speed=$playbackSpeed pitch=$pitch " +
                    "articulation=$articulationScale pause=${scaledPauseMs}ms"
            )
            play(output, generation, playbackSpeed, pitch, scaledPauseMs)
        }
    }

    fun stop() {
        playbackGeneration.incrementAndGet()
        synchronized(trackLock) {
            activeTrack?.runCatching { pause() }
            activeTrack?.runCatching { flush() }
            activeTrack?.runCatching { stop() }
            activeTrack?.release()
            activeTrack = null
        }
    }

    fun destroy() {
        stop()
        dictionary?.close()
        dictionary = null
        tokenizer = null
        bertSession?.close()
        bertSession = null
        synthesisSession?.close()
        synthesisSession = null
    }

    private fun prepareInput(
        text: String,
        dictionary: PronunciationDictionary,
        tokenizer: WordPieceTokenizer,
        bert: OrtSession
    ): PreparedInput {
        val normalized = RussianNumberNormalizer.normalize(text)
            .replace(Regex("([А-Яа-яЁё])\\u0301"), "+$1")
            .lowercase(Locale.forLanguageTag("ru"))
            .replace('—', '-')
            .replace('–', '-')
            .replace("…", "...")
            .replace(Regex("[«»“”]"), "\"")
            .replace(Regex("\\s+"), " ")
            .trim()
        val encoding = tokenizer.encode(normalized)
        val tokenEmbeddings = runBert(bert, encoding.ids)
        val selectedEmbeddings = encoding.embeddingPositions.map { position -> tokenEmbeddings[position] }
        val phones = buildMultistreamPhones(normalized, dictionary)
        val timeSteps = phones.size
        val features = LongArray(FEATURE_CHANNELS * timeSteps)
        val embeddings = FloatArray(BERT_DIMENSIONS * timeSteps)
        phones.forEachIndexed { time, phone ->
            phone.features.forEachIndexed { channel, value -> features[channel * timeSteps + time] = value }
            val embedding = selectedEmbeddings[phone.bertWordIndex.coerceIn(selectedEmbeddings.indices)]
            for (channel in 0 until BERT_DIMENSIONS) {
                embeddings[channel * timeSteps + time] = embedding[channel]
            }
        }
        return PreparedInput(features, embeddings, timeSteps)
    }

    private fun runBert(session: OrtSession, ids: LongArray): Array<FloatArray> {
        val shape = longArrayOf(1, ids.size.toLong())
        val inputIds = OnnxTensor.createTensor(environment, LongBuffer.wrap(ids), shape)
        val attentionMask = OnnxTensor.createTensor(
            environment,
            LongBuffer.wrap(LongArray(ids.size) { 1L }),
            shape
        )
        val tokenTypes = OnnxTensor.createTensor(environment, LongBuffer.wrap(LongArray(ids.size)), shape)
        try {
            session.run(
                mapOf(
                    "input_ids" to inputIds,
                    "attention_mask" to attentionMask,
                    "token_type_ids" to tokenTypes
                )
            ).use { result ->
                @Suppress("UNCHECKED_CAST")
                return result[0].value as Array<FloatArray>
            }
        } finally {
            inputIds.close()
            attentionMask.close()
            tokenTypes.close()
        }
    }

    private fun synthesize(session: OrtSession, prepared: PreparedInput, articulationScale: Float): FloatArray {
        val input = OnnxTensor.createTensor(
            environment,
            LongBuffer.wrap(prepared.features),
            longArrayOf(1, FEATURE_CHANNELS.toLong(), prepared.timeSteps.toLong())
        )
        val inputLengths = OnnxTensor.createTensor(
            environment,
            LongBuffer.wrap(longArrayOf(prepared.timeSteps.toLong())),
            longArrayOf(1)
        )
        val scales = OnnxTensor.createTensor(
            environment,
            FloatBuffer.wrap(
                floatArrayOf(
                    NOISE_SCALE,
                    articulationScale.coerceIn(MIN_ARTICULATION_SCALE, MAX_ARTICULATION_SCALE),
                    DURATION_NOISE_SCALE
                )
            ),
            longArrayOf(3)
        )
        val speaker = OnnxTensor.createTensor(
            environment,
            LongBuffer.wrap(longArrayOf(MALE_1_SPEAKER_ID.toLong())),
            longArrayOf(1)
        )
        val bert = OnnxTensor.createTensor(
            environment,
            FloatBuffer.wrap(prepared.bertEmbeddings),
            longArrayOf(1, BERT_DIMENSIONS.toLong(), prepared.timeSteps.toLong())
        )
        try {
            session.run(
                mapOf(
                    "input" to input,
                    "input_lengths" to inputLengths,
                    "scales" to scales,
                    "sid" to speaker,
                    "bert" to bert
                )
            ).use { result ->
                return flattenFloatOutput(result[0].value)
            }
        } finally {
            input.close()
            inputLengths.close()
            scales.close()
            speaker.close()
            bert.close()
        }
    }

    private fun buildMultistreamPhones(
        text: String,
        dictionary: PronunciationDictionary
    ): List<PhoneFeatures> {
        val rawPhones = mutableListOf(RawPhone("^", emptyList(), 0, 0))
        val word = StringBuilder()
        val pendingPunctuation = mutableListOf<String>()
        var inQuote = 0
        var bertWordIndex = 1
        fun flushWord() {
            if (word.isEmpty()) return
            val value = word.toString()
            val pronunciation = dictionary.find(value) ?: RussianG2p.convert(value)
            pronunciation.split(' ').filter(String::isNotBlank).forEach { phone ->
                rawPhones += RawPhone(phone, emptyList(), inQuote, bertWordIndex)
            }
            word.clear()
            bertWordIndex += 1
        }
        fun appendSpace() {
            rawPhones += RawPhone(" ", pendingPunctuation.toList(), inQuote, bertWordIndex)
            pendingPunctuation.clear()
        }
        var index = 0
        while (index < text.length) {
            if (text.startsWith("...", index)) {
                flushWord()
                pendingPunctuation += "..."
                index += 3
                continue
            }
            val char = text[index]
            when {
                char == '"' || char == '«' || char == '»' || char == '“' || char == '”' -> {
                    flushWord()
                    inQuote = 1 - inQuote
                }
                char.isWhitespace() -> {
                    flushWord()
                    appendSpace()
                }
                char == '-' && text.getOrNull(index - 1)?.isLetterOrDigit() == true &&
                    text.getOrNull(index + 1)?.isLetterOrDigit() == true -> {
                    // A lexical hyphen separates WordPiece words but must not create an acoustic pause.
                    flushWord()
                }
                char in MULTISTREAM_PUNCTUATION -> {
                    flushWord()
                    pendingPunctuation += char.toString()
                }
                char.isLetterOrDigit() || char == '+' -> word.append(char)
            }
            index += 1
        }
        flushWord()
        appendSpace()
        rawPhones += RawPhone("$", emptyList(), 0, bertWordIndex)

        var lastPunctuation = " "
        var lastSentencePunctuation = " "
        val reversed = mutableListOf<PhoneFeatures>()
        for (phone in rawPhones.asReversed()) {
            lastSentencePunctuation = when {
                "..." in phone.punctuation -> "..."
                "." in phone.punctuation -> "."
                "!" in phone.punctuation -> "!"
                "?" in phone.punctuation -> "?"
                "-" in phone.punctuation -> "-"
                else -> lastSentencePunctuation
            }
            if (phone.punctuation.isNotEmpty()) lastPunctuation = phone.punctuation.first()
            val currentPunctuation = phone.punctuation.firstOrNull() ?: "_"
            val values = longArrayOf(
                PHONEME_IDS.getValue(phone.phone),
                PHONEME_IDS.getValue(currentPunctuation),
                phone.inQuote.toLong(),
                PHONEME_IDS.getValue(lastPunctuation),
                PHONEME_IDS.getValue(lastSentencePunctuation)
            )
            reversed += PhoneFeatures(values, phone.bertWordIndex)
        }
        return reversed.asReversed()
    }

    private fun flattenFloatOutput(value: Any?): FloatArray = when (value) {
        is FloatArray -> value
        is Array<*> -> value.asSequence().flatMap { flattenFloatOutput(it).asSequence() }.toList().toFloatArray()
        else -> error("Unexpected Vosk output type: ${value?.javaClass?.name}")
    }

    private fun play(
        samples: FloatArray,
        generation: Int,
        playbackSpeed: Float,
        pitch: Float,
        trailingPauseMs: Int
    ) {
        val minBytes = AudioTrack.getMinBufferSize(
            SAMPLE_RATE,
            AudioFormat.CHANNEL_OUT_MONO,
            AudioFormat.ENCODING_PCM_FLOAT
        ).coerceAtLeast(4096)
        val track = AudioTrack.Builder()
            .setAudioAttributes(
                AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                    .build()
            )
            .setAudioFormat(
                AudioFormat.Builder()
                    .setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
                    .setSampleRate(SAMPLE_RATE)
                    .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                    .build()
            )
            .setBufferSizeInBytes(minBytes)
            .setTransferMode(AudioTrack.MODE_STREAM)
            .build()
        track.playbackParams = PlaybackParams()
            .allowDefaults()
            .setSpeed(playbackSpeed)
            .setPitch(pitch.coerceIn(MIN_PLAYBACK_PITCH, MAX_PLAYBACK_PITCH))
        synchronized(trackLock) { activeTrack = track }
        try {
            track.play()
            var offset = 0
            while (offset < samples.size && generation == playbackGeneration.get()) {
                val written = track.write(samples, offset, samples.size - offset, AudioTrack.WRITE_BLOCKING)
                if (written <= 0) error("AudioTrack write failed: $written")
                offset += written
            }
            var silenceSamples = trailingPauseMs * SAMPLE_RATE / 1_000
            while (silenceSamples > 0 && generation == playbackGeneration.get()) {
                val count = minOf(silenceSamples, SILENCE_BUFFER.size)
                val written = track.write(SILENCE_BUFFER, 0, count, AudioTrack.WRITE_BLOCKING)
                if (written <= 0) error("AudioTrack silence write failed: $written")
                silenceSamples -= written
            }
            check(generation == playbackGeneration.get()) { "Playback stopped" }
        } finally {
            synchronized(trackLock) { if (activeTrack === track) activeTrack = null }
            track.runCatching { stop() }
            track.release()
        }
    }

    private fun splitText(text: String): List<SpeechChunk> {
        val normalized = text
            .replace(Regex("\\[\\d+(?:[,–-]\\d+)*\\]"), " ")
            .replace("\r\n", "\n")
            .replace('\r', '\n')
            .replace(Regex("[\\t\\x0B\\f ]+"), " ")
            .replace(Regex(" *\\n+ *"), "\n")
            .trim()
        if (normalized.isBlank()) return emptyList()
        val chunks = mutableListOf<SpeechChunk>()
        for (paragraph in normalized.split('\n').filter(String::isNotBlank)) {
            for (sentence in RussianSpeechTextSplitter.splitSentences(paragraph)) {
                for (unit in splitControlledPauses(sentence)) {
                    val parts = splitAtWordBoundaries(unit.text)
                    parts.forEachIndexed { index, part ->
                        val isLastPart = index == parts.lastIndex
                        chunks += SpeechChunk(
                            text = part.trim(),
                            trailingPauseMs = if (isLastPart) unit.trailingPauseMs else NO_TRAILING_PAUSE_MS,
                            maximumPauseMs = if (isLastPart) unit.maximumPauseMs else NO_TRAILING_PAUSE_MS
                        )
                    }
                }
            }
        }
        return chunks.filter { it.text.isNotBlank() }
    }

    private fun splitControlledPauses(text: String): List<ProsodicUnit> {
        val units = mutableListOf<ProsodicUnit>()
        val current = StringBuilder()
        fun flushControlledPause() {
            val value = current.toString().trim()
            if (value.isNotBlank()) {
                units += ProsodicUnit(value, CONTROLLED_PUNCTUATION_PAUSE_MS, CONTROLLED_PUNCTUATION_MAX_PAUSE_MS)
            }
            current.clear()
        }
        text.forEachIndexed { index, char ->
            val isWordInternalHyphen = char == '-' &&
                text.getOrNull(index - 1)?.isLetterOrDigit() == true &&
                text.getOrNull(index + 1)?.isLetterOrDigit() == true
            when {
                char in CONTROLLED_PAUSE_PUNCTUATION && !isWordInternalHyphen -> flushControlledPause()
                else -> current.append(char)
            }
        }
        val remainder = current.toString().trim()
        if (remainder.isNotBlank()) units += ProsodicUnit(remainder, pauseAfter(remainder), Int.MAX_VALUE)
        return units
    }

    private fun splitAtWordBoundaries(text: String): List<String> {
        if (text.length <= MAX_CHUNK_CHARS) return listOf(text)
        val parts = mutableListOf<String>()
        var remaining = text.trim()
        while (remaining.length > MAX_CHUNK_CHARS) {
            val boundary = remaining.lastIndexOf(' ', startIndex = MAX_CHUNK_CHARS)
                .takeIf { it > MIN_CHUNK_CHARS }
                ?: remaining.indexOf(' ', startIndex = MAX_CHUNK_CHARS).takeIf { it >= 0 }
                ?: remaining.length
            parts += remaining.substring(0, boundary).trim()
            remaining = remaining.substring(boundary).trimStart()
        }
        if (remaining.isNotBlank()) parts += remaining
        return parts
    }

    private fun pauseAfter(text: String): Int = when (text.trimEnd().lastOrNull()) {
        '…' -> ELLIPSIS_PAUSE_MS
        '?' -> QUESTION_PAUSE_MS
        '!' -> EXCLAMATION_PAUSE_MS
        '.' -> SENTENCE_PAUSE_MS
        ':', ';' -> CLAUSE_PAUSE_MS
        else -> NO_TRAILING_PAUSE_MS
    }

    private fun copyAssetToInternalStorage(assetPath: String, fileName: String): File {
        val destination = File(appContext.filesDir, "tts/vosk_0_9/$fileName")
        val expectedSize = appContext.assets.open(assetPath).use { input ->
            var total = 0L
            val buffer = ByteArray(COPY_BUFFER_SIZE)
            while (true) {
                val read = input.read(buffer)
                if (read < 0) break
                total += read
            }
            total
        }
        if (destination.isFile && destination.length() == expectedSize) return destination
        destination.parentFile?.mkdirs()
        val temporary = File(destination.parentFile, "$fileName.tmp")
        appContext.assets.open(assetPath).use { input ->
            FileOutputStream(temporary).use { output -> input.copyTo(output, COPY_BUFFER_SIZE) }
        }
        check(temporary.length() == expectedSize) { "Vosk asset copy is incomplete: $fileName" }
        if (destination.exists()) check(destination.delete()) { "Cannot replace old Vosk asset: $fileName" }
        check(temporary.renameTo(destination)) { "Cannot install Vosk asset: $fileName" }
        return destination
    }

    private class WordPieceTokenizer(private val vocabulary: Map<String, Long>) {
        fun encode(text: String): BertEncoding {
            val ids = mutableListOf(vocabulary.getValue("[CLS]"))
            val embeddingPositions = mutableListOf(0)
            for (token in basicTokens(text)) {
                val punctuation = token in BERT_EXCLUDED_PUNCTUATION
                val pieces = if (punctuation) {
                    listOf(vocabulary[token] ?: vocabulary.getValue("[UNK]"))
                } else {
                    wordPieces(token)
                }
                if (!punctuation) embeddingPositions += ids.size
                ids += pieces
            }
            ids += vocabulary.getValue("[SEP]")
            embeddingPositions += ids.lastIndex
            return BertEncoding(ids.toLongArray(), embeddingPositions.toIntArray())
        }

        private fun basicTokens(text: String): List<String> {
            val result = mutableListOf<String>()
            val word = StringBuilder()
            fun flush() {
                if (word.isNotEmpty()) result += word.toString().also { word.clear() }
            }
            text.forEach { char ->
                when {
                    char.isWhitespace() -> flush()
                    char.isLetterOrDigit() -> word.append(char.lowercaseChar())
                    char == '+' -> Unit // Explicit stress marker belongs to G2P, not to BERT tokenization.
                    else -> {
                        flush()
                        result += char.toString()
                    }
                }
            }
            flush()
            return result
        }

        private fun wordPieces(token: String): List<Long> {
            if (token.length > MAX_WORD_PIECE_CHARS) return listOf(vocabulary.getValue("[UNK]"))
            val result = mutableListOf<Long>()
            var start = 0
            while (start < token.length) {
                var end = token.length
                var found: Long? = null
                while (start < end) {
                    val piece = token.substring(start, end).let { if (start == 0) it else "##$it" }
                    found = vocabulary[piece]
                    if (found != null) break
                    end -= 1
                }
                if (found == null) return listOf(vocabulary.getValue("[UNK]"))
                result += found
                start = end
            }
            return result
        }
    }

    private data class DictionaryIndexEntry(val word: String, val offset: Long)

    private class PronunciationDictionary(
        file: File,
        private val index: List<DictionaryIndexEntry>
    ) : AutoCloseable {
        private val source = RandomAccessFile(file, "r")
        private val cache = object : LinkedHashMap<String, String>(CACHE_SIZE, 0.75f, true) {
            override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, String>?): Boolean = size > CACHE_SIZE
        }

        @Synchronized
        fun find(word: String): String? {
            cache[word]?.let { return it }
            var low = 0
            var high = index.lastIndex
            while (low <= high) {
                val midpoint = (low + high) ushr 1
                if (index[midpoint].word <= word) low = midpoint + 1 else high = midpoint - 1
            }
            val rangeIndex = high.coerceAtLeast(0)
            val rangeEnd = index.getOrNull(rangeIndex + 1)?.offset ?: source.length()
            source.seek(index[rangeIndex].offset)
            while (source.filePointer < rangeEnd) {
                val line = readUtf8Line(source) ?: break
                val entryWord = line.substringBefore('\t')
                when {
                    entryWord < word -> continue
                    entryWord > word -> return null
                    else -> {
                        val pronunciation = line.substringAfter('\t', "").trim()
                        if (pronunciation.isNotEmpty()) cache[word] = pronunciation
                        return pronunciation.takeIf(String::isNotEmpty)
                    }
                }
            }
            return null
        }

        override fun close() = source.close()

        private fun readUtf8Line(file: RandomAccessFile): String? {
            val bytes = ByteArrayOutputStream(64)
            while (true) {
                val value = file.read()
                if (value < 0) return if (bytes.size() == 0) null else bytes.toString(StandardCharsets.UTF_8.name())
                if (value == '\n'.code) break
                if (value != '\r'.code) bytes.write(value)
            }
            return bytes.toString(StandardCharsets.UTF_8.name())
        }
    }

    private object RussianG2p {
        private val softLetters = setOf('я', 'ё', 'ю', 'и', 'ь', 'е')
        private val syllableStarts = setOf('#', 'ъ', 'ь', 'а', 'я', 'о', 'ё', 'у', 'ю', 'э', 'е', 'и', 'ы', '-')
        private val softHardConsonants = mapOf(
            'б' to "b", 'в' to "v", 'г' to "g", 'д' to "d", 'з' to "z", 'к' to "k",
            'л' to "l", 'м' to "m", 'н' to "n", 'п' to "p", 'р' to "r", 'с' to "s",
            'т' to "t", 'ф' to "f", 'х' to "h"
        )
        private val otherConsonants = mapOf('ж' to "zh", 'ц' to "c", 'ч' to "ch", 'ш' to "sh", 'щ' to "sch", 'й' to "j")
        private val vowels = mapOf('а' to "a", 'я' to "a", 'у' to "u", 'ю' to "u", 'о' to "o", 'ё' to "o", 'э' to "e", 'е' to "e", 'и' to "i", 'ы' to "y")

        fun convert(word: String): String {
            val marked = mutableListOf<Pair<Char, Int>>()
            var stress = 0
            for (char in "#$word#") {
                if (char == '+') stress = 1 else {
                    marked += char to stress
                    stress = 0
                }
            }
            val phones = mutableListOf<String>()
            for (index in marked.indices) {
                val (char, accent) = marked[index]
                val next = marked.getOrNull(index + 1)?.first
                when {
                    char in softHardConsonants -> phones += softHardConsonants.getValue(char) + if (next in softLetters) "j" else ""
                    char in otherConsonants -> phones += otherConsonants.getValue(char)
                    char in vowels -> {
                        val previous = marked.getOrNull(index - 1)?.first ?: '#'
                        if (previous in syllableStarts && char in setOf('я', 'ю', 'е', 'ё')) phones += "j"
                        phones += vowels.getValue(char) + accent
                    }
                }
            }
            return phones.joinToString(" ")
        }
    }

    private companion object {
        data class PreparedInput(
            val features: LongArray,
            val bertEmbeddings: FloatArray,
            val timeSteps: Int
        )

        data class BertEncoding(val ids: LongArray, val embeddingPositions: IntArray)
        data class RawPhone(val phone: String, val punctuation: List<String>, val inQuote: Int, val bertWordIndex: Int)
        data class PhoneFeatures(val features: LongArray, val bertWordIndex: Int)
        data class SpeechChunk(val text: String, val trailingPauseMs: Int, val maximumPauseMs: Int = Int.MAX_VALUE)
        data class ProsodicUnit(val text: String, val trailingPauseMs: Int, val maximumPauseMs: Int)

        const val TAG = "AletheiaVoskTts"
        const val MODEL_FILE = "model.onnx"
        const val BERT_FILE = "bert.int8.onnx"
        const val DICTIONARY_FILE = "dict.tsv"
        const val MODEL_ASSET = "tts/vosk_0_9/$MODEL_FILE"
        const val BERT_ASSET = "tts/vosk_0_9/$BERT_FILE"
        const val DICTIONARY_ASSET = "tts/vosk_0_9/$DICTIONARY_FILE"
        const val DICTIONARY_INDEX_ASSET = "tts/vosk_0_9/dictionary.index"
        const val VOCABULARY_ASSET = "tts/vosk_0_9/vocab.txt"
        const val SAMPLE_RATE = 22_050
        const val MALE_1_SPEAKER_ID = 4
        const val FEATURE_CHANNELS = 5
        const val BERT_DIMENSIONS = 768
        const val INFERENCE_THREADS = 4
        const val COPY_BUFFER_SIZE = 1024 * 1024
        const val CACHE_SIZE = 4_096
        const val MAX_WORD_PIECE_CHARS = 100
        const val MAX_CHUNK_CHARS = 120
        const val MIN_CHUNK_CHARS = 80
        const val BOOK_SPEED_SCALE = 0.82f
        const val MIN_RATE_FOR_PAUSE_SCALING = 0.10f
        const val MIN_PLAYBACK_SPEED = 0.50f
        const val MAX_PLAYBACK_SPEED = 1.50f
        const val MIN_PLAYBACK_PITCH = 0.75f
        const val MAX_PLAYBACK_PITCH = 1.30f
        const val MIN_ARTICULATION_SCALE = 0.90f
        const val MAX_ARTICULATION_SCALE = 1.20f
        const val NOISE_SCALE = 0.8f
        const val DURATION_NOISE_SCALE = 0.8f
        const val NO_TRAILING_PAUSE_MS = 0
        const val MIN_TRAILING_PAUSE_MS = 80
        const val CLAUSE_PAUSE_MS = 320
        const val CONTROLLED_PUNCTUATION_PAUSE_MS = 300
        const val CONTROLLED_PUNCTUATION_MAX_PAUSE_MS = 300
        const val SENTENCE_PAUSE_MS = 600
        const val EXCLAMATION_PAUSE_MS = 600
        const val QUESTION_PAUSE_MS = 600
        const val ELLIPSIS_PAUSE_MS = 1_200

        val SILENCE_BUFFER = FloatArray(SAMPLE_RATE / 10)
        val CONTROLLED_PAUSE_PUNCTUATION = setOf('—', '–', '-', '(', ')', ';', ':')
        val MULTISTREAM_PUNCTUATION = setOf('!', '(', ')', ',', '-', '.', ':', ';', '?')
        val BERT_EXCLUDED_PUNCTUATION = setOf("-", ",", ".", "?", "!", ";", ":", "\"")
        val PHONEME_IDS = listOf(
            "_", "^", "$", " ", "!", "'", "(", ")", ",", "-", ".", "...", ":", ";", "?",
            "a0", "a1", "b", "bj", "c", "ch", "d", "dj", "e0", "e1", "f", "fj", "g", "gj",
            "h", "hj", "i0", "i1", "j", "k", "kj", "l", "lj", "m", "mj", "n", "nj", "o0",
            "o1", "p", "pj", "r", "rj", "s", "sch", "sh", "sj", "t", "tj", "u0", "u1", "v",
            "vj", "y0", "y1", "z", "zh", "zj"
        ).mapIndexed { index, phoneme -> phoneme to index.toLong() }.toMap()
    }
}
