import 'dart:io'; import 'dart:convert'; import 'dart:math' as math; import 'dart:typed_data'; import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_onnxruntime/flutter_onnxruntime.dart'; import 'package:logger/logger.dart'; import 'package:path_provider/path_provider.dart'; final logger = Logger( printer: PrettyPrinter(methodCount: 0, errorMethodCount: 5, lineLength: 80), ); class UnicodeProcessor { final Map indexer; UnicodeProcessor._(this.indexer); static Future load(String path) async { final json = jsonDecode( path.startsWith('assets/') ? await rootBundle.loadString(path) : File(path).readAsStringSync(), ); final indexer = json is List ? { for (var i = 0; i < json.length; i++) if (json[i] is int && json[i] >= 0) i: json[i] as int } : (json as Map) .map((k, v) => MapEntry(int.parse(k), v as int)); return UnicodeProcessor._(indexer); } Map call(List textList) { final lengths = textList.map((t) => t.length).toList(); final maxLen = lengths.reduce(math.max); final textIds = textList.map((text) { final row = List.filled(maxLen, 0); final runes = text.runes.toList(); for (var i = 0; i < runes.length; i++) { row[i] = indexer[runes[i]] ?? 0; } return row; }).toList(); return {'textIds': textIds, 'textMask': _lengthToMask(lengths)}; } List>> _lengthToMask(List lengths, [int? maxLen]) { maxLen ??= lengths.reduce(math.max); return lengths .map((len) => [List.generate(maxLen!, (i) => i < len ? 1.0 : 0.0)]) .toList(); } } class Style { final OrtValue ttl, dp; final List ttlShape, dpShape; Style(this.ttl, this.dp, this.ttlShape, this.dpShape); } class TextToSpeech { final Map cfgs; final UnicodeProcessor textProcessor; final OrtSession dpOrt, textEncOrt, vectorEstOrt, vocoderOrt; final int sampleRate, baseChunkSize, chunkCompressFactor, ldim; TextToSpeech(this.cfgs, this.textProcessor, this.dpOrt, this.textEncOrt, this.vectorEstOrt, this.vocoderOrt) : sampleRate = cfgs['ae']['sample_rate'], baseChunkSize = cfgs['ae']['base_chunk_size'], chunkCompressFactor = cfgs['ttl']['chunk_compress_factor'], ldim = cfgs['ttl']['latent_dim']; Future> call(String text, Style style, int totalStep, {double speed = 1.05, double silenceDuration = 0.3}) async { final chunks = _chunkText(text); List? wavCat; double durCat = 0; for (final chunk in chunks) { final result = await _infer([chunk], style, totalStep, speed: speed); final wav = _safeCast(result['wav']); final duration = _safeCast(result['duration']); if (wavCat == null) { wavCat = wav; durCat = duration[0]; } else { wavCat = [ ...wavCat, ...List.filled((silenceDuration * sampleRate).floor(), 0.0), ...wav ]; durCat += duration[0] + silenceDuration; } } return { 'wav': wavCat, 'duration': [durCat] }; } Future> _infer( List textList, Style style, int totalStep, {double speed = 1.05}) async { final bsz = textList.length; final result = textProcessor.call(textList); final textIdsRaw = result['textIds']; final textIds = textIdsRaw is List> ? textIdsRaw : (textIdsRaw as List).map((row) => (row as List).cast()).toList(); final textMaskRaw = result['textMask']; final textMask = textMaskRaw is List>> ? textMaskRaw : (textMaskRaw as List) .map((batch) => (batch as List) .map((row) => (row as List).cast()) .toList()) .toList(); final textIdsShape = [bsz, textIds[0].length]; final textMaskShape = [bsz, 1, textMask[0][0].length]; final textMaskTensor = await _toTensor(textMask, textMaskShape); final dpResult = await dpOrt.run({ 'text_ids': await _intToTensor(textIds, textIdsShape), 'style_dp': style.dp, 'text_mask': textMaskTensor, }); final durOnnx = _safeCast(await dpResult.values.first.asList()); final scaledDur = durOnnx.map((d) => d / speed).toList(); final textEncResult = await textEncOrt.run({ 'text_ids': await _intToTensor(textIds, textIdsShape), 'style_ttl': style.ttl, 'text_mask': textMaskTensor, }); final latentData = _sampleNoisyLatent(scaledDur); final noisyLatentRaw = latentData['noisyLatent']; var noisyLatent = noisyLatentRaw is List>> ? noisyLatentRaw : (noisyLatentRaw as List) .map((batch) => (batch as List) .map((row) => (row as List).cast()) .toList()) .toList(); final latentMaskRaw = latentData['latentMask']; final latentMask = latentMaskRaw is List>> ? latentMaskRaw : (latentMaskRaw as List) .map((batch) => (batch as List) .map((row) => (row as List).cast()) .toList()) .toList(); final latentShape = [bsz, noisyLatent[0].length, noisyLatent[0][0].length]; final latentMaskTensor = await _toTensor(latentMask, [bsz, 1, latentMask[0][0].length]); final totalStepTensor = await _scalarToTensor(List.filled(bsz, totalStep.toDouble()), [bsz]); // Denoising loop for (var step = 0; step < totalStep; step++) { final result = await vectorEstOrt.run({ 'noisy_latent': await _toTensor(noisyLatent, latentShape), 'text_emb': textEncResult.values.first, 'style_ttl': style.ttl, 'text_mask': textMaskTensor, 'latent_mask': latentMaskTensor, 'total_step': totalStepTensor, 'current_step': await _scalarToTensor(List.filled(bsz, step.toDouble()), [bsz]), }); final denoisedRaw = await result.values.first.asList(); final denoised = denoisedRaw is List ? denoisedRaw : _safeCast(denoisedRaw); var idx = 0; for (var b = 0; b < noisyLatent.length; b++) { for (var d = 0; d < noisyLatent[b].length; d++) { for (var t = 0; t < noisyLatent[b][d].length; t++) { noisyLatent[b][d][t] = denoised[idx++]; } } } } final vocoderResult = await vocoderOrt .run({'latent': await _toTensor(noisyLatent, latentShape)}); final wavRaw = await vocoderResult.values.first.asList(); final wav = wavRaw is List ? wavRaw : _safeCast(wavRaw); return {'wav': wav, 'duration': scaledDur}; } Map _sampleNoisyLatent(List duration) { final wavLenMax = duration.reduce(math.max) * sampleRate; final wavLengths = duration.map((d) => (d * sampleRate).floor()).toList(); final chunkSize = baseChunkSize * chunkCompressFactor; final latentLen = ((wavLenMax + chunkSize - 1) / chunkSize).floor(); final latentDim = ldim * chunkCompressFactor; final random = math.Random(); final noisyLatent = List.generate( duration.length, (_) => List.generate( latentDim, (_) => List.generate(latentLen, (_) { final u1 = math.max(1e-10, random.nextDouble()); final u2 = random.nextDouble(); return math.sqrt(-2.0 * math.log(u1)) * math.cos(2.0 * math.pi * u2); }), ), ); final latentMask = _getLatentMask(wavLengths); for (var b = 0; b < noisyLatent.length; b++) { for (var d = 0; d < noisyLatent[b].length; d++) { for (var t = 0; t < noisyLatent[b][d].length; t++) { noisyLatent[b][d][t] *= latentMask[b][0][t]; } } } return {'noisyLatent': noisyLatent, 'latentMask': latentMask}; } List>> _getLatentMask(List wavLengths) { final latentSize = baseChunkSize * chunkCompressFactor; final latentLengths = wavLengths .map((len) => ((len + latentSize - 1) / latentSize).floor()) .toList(); final maxLen = latentLengths.reduce(math.max); return latentLengths .map((len) => [List.generate(maxLen, (i) => i < len ? 1.0 : 0.0)]) .toList(); } List _chunkText(String text, {int maxLen = 300}) { final paragraphs = text .trim() .split(RegExp(r'\n\s*\n+')) .where((p) => p.trim().isNotEmpty) .toList(); final chunks = []; for (var paragraph in paragraphs) { paragraph = paragraph.trim(); if (paragraph.isEmpty) continue; final sentences = paragraph.split(RegExp( r'(? _safeCast(dynamic raw) { if (raw is List) return raw; if (raw is List) { if (raw.isNotEmpty && raw.first is List) { return _flattenList(raw); } if (T == double) { return raw .map((e) => e is num ? e.toDouble() : double.parse(e.toString())) .toList() as List; } return raw.cast(); } throw Exception('Cannot convert $raw to List<$T>'); } List _flattenList(dynamic list) { if (list is List) { return list.expand((e) => _flattenList(e)).toList(); } if (T == double && list is num) { return [list.toDouble()] as List; } return [list as T]; } Future _toTensor(dynamic array, List dims) async { final flat = _flattenList(array); return await OrtValue.fromList(Float32List.fromList(flat), dims); } Future _scalarToTensor(List array, List dims) async { return await OrtValue.fromList(Float32List.fromList(array), dims); } Future _intToTensor(List> array, List dims) async { final flat = array.expand((row) => row).toList(); return await OrtValue.fromList(Int64List.fromList(flat), dims); } } Future loadTextToSpeech(String onnxDir, {bool useGpu = false}) async { if (useGpu) throw Exception('GPU mode not supported yet'); logger.i('Loading TTS models from $onnxDir'); final cfgs = await _loadCfgs(onnxDir); final sessions = await _loadOnnxAll(onnxDir); final textProcessor = await UnicodeProcessor.load('$onnxDir/unicode_indexer.json'); logger.i('TTS models loaded successfully'); return TextToSpeech( cfgs, textProcessor, sessions['dpOrt']!, sessions['textEncOrt']!, sessions['vectorEstOrt']!, sessions['vocoderOrt']!, ); } Future