initial commit

This commit is contained in:
2026-01-25 18:58:40 +09:00
commit 77af47274c
101 changed files with 16247 additions and 0 deletions

391
flutter/lib/main.dart Normal file
View File

@@ -0,0 +1,391 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_sdk/helper.dart';
void main() {
runApp(const SupertonicApp());
}
class SupertonicApp extends StatelessWidget {
const SupertonicApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Supertonic 2',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const TTSPage(),
);
}
}
class TTSPage extends StatefulWidget {
const TTSPage({super.key});
@override
State<TTSPage> createState() => _TTSPageState();
}
class _TTSPageState extends State<TTSPage> {
final TextEditingController _textController = TextEditingController(
text: 'Hello, this is a text to speech example.',
);
final AudioPlayer _audioPlayer = AudioPlayer();
TextToSpeech? _textToSpeech;
Style? _style;
bool _isLoading = false;
bool _isGenerating = false;
String _status = 'Not initialized';
int _totalSteps = 5;
double _speed = 1.05;
String _selectedLang = 'en';
bool _isPlaying = false;
String? _lastGeneratedFilePath;
@override
void initState() {
super.initState();
_loadModels();
_setupAudioPlayerListeners();
}
void _setupAudioPlayerListeners() {
_audioPlayer.playerStateStream.listen((state) {
if (!mounted) return;
setState(() {
_isPlaying = state.playing;
if (state.processingState == ProcessingState.completed) {
_isPlaying = false;
_status = 'Ready';
} else if (state.processingState == ProcessingState.loading) {
_status = 'Loading audio...';
} else if (state.processingState == ProcessingState.buffering) {
_status = 'Buffering...';
}
});
});
}
Future<void> _loadModels() async {
setState(() {
_isLoading = true;
_status = 'Loading models...';
});
try {
_textToSpeech = await loadTextToSpeech('assets/onnx', useGpu: false);
_style = await loadVoiceStyle(['assets/voice_styles/M1.json']);
setState(() {
_isLoading = false;
_status = 'Ready';
});
} catch (e, stackTrace) {
logger.e('Error loading models', error: e, stackTrace: stackTrace);
setState(() {
_isLoading = false;
_status = 'Error: $e';
});
}
}
Future<void> _generateSpeech() async {
if (_textToSpeech == null || _style == null) {
setState(() => _status = 'Models not loaded yet');
return;
}
if (_textController.text.trim().isEmpty) {
setState(() => _status = 'Please enter some text');
return;
}
setState(() {
_isGenerating = true;
_status = 'Generating speech...';
});
List<double>? wav;
List<double>? duration;
// Step 1: Generate speech
try {
final result = await _textToSpeech!.call(
_textController.text,
_selectedLang,
_style!,
_totalSteps,
speed: _speed,
);
wav = result['wav'] is List<double>
? result['wav']
: (result['wav'] as List).cast<double>();
duration = result['duration'] is List<double>
? result['duration']
: (result['duration'] as List).cast<double>();
} catch (e) {
logger.e('Error generating speech', error: e);
setState(() {
_isGenerating = false;
_status = 'Error generating speech: $e';
});
return;
}
// Step 2: Save to file and play
try {
final tempDir = await getTemporaryDirectory();
final timestamp = DateTime.now().millisecondsSinceEpoch;
final outputPath = '${tempDir.path}/speech_$timestamp.wav';
writeWavFile(outputPath, wav!, _textToSpeech!.sampleRate);
final file = File(outputPath);
if (!file.existsSync()) {
throw Exception('Failed to create WAV file');
}
final absolutePath = file.absolute.path;
setState(() {
_isGenerating = false;
_status = 'Playing ${duration![0].toStringAsFixed(2)}s of audio...';
_lastGeneratedFilePath = absolutePath;
});
logger.i('Audio saved to $absolutePath');
final uri = Uri.file(absolutePath);
await _audioPlayer.setAudioSource(AudioSource.uri(uri));
await _audioPlayer.play();
} catch (e) {
logger.e('Error playing audio', error: e);
setState(() {
_isGenerating = false;
_status = 'Error playing audio: $e';
});
}
}
Future<void> _downloadFile() async {
if (_lastGeneratedFilePath == null) return;
try {
final sourceFile = File(_lastGeneratedFilePath!);
if (!sourceFile.existsSync()) {
setState(() => _status = 'Error: File no longer exists');
return;
}
final downloadsDir = await getDownloadsDirectory();
if (downloadsDir == null) {
setState(() => _status = 'Error: Could not access downloads folder');
return;
}
final timestamp = DateTime.now().millisecondsSinceEpoch;
final downloadPath = '${downloadsDir.path}/speech_$timestamp.wav';
await sourceFile.copy(downloadPath);
logger.i('File saved to $downloadPath');
setState(() => _status = 'File saved to: $downloadPath');
} catch (e) {
logger.e('Error downloading file', error: e);
setState(() => _status = 'Error downloading file: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Supertonic 2'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Status indicator
Card(
color: _isLoading || _isGenerating
? Colors.orange.shade100
: _status.startsWith('Error')
? Colors.red.shade100
: Colors.green.shade100,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
if (_isLoading || _isGenerating)
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
if (_isLoading || _isGenerating) const SizedBox(width: 12),
Expanded(
child:
Text(_status, style: const TextStyle(fontSize: 16)),
),
],
),
),
),
const SizedBox(height: 24),
// Text input
TextField(
controller: _textController,
maxLines: 5,
decoration: const InputDecoration(
labelText: 'Text to synthesize',
border: OutlineInputBorder(),
hintText: 'Enter the text you want to convert to speech...',
),
enabled: !_isLoading && !_isGenerating,
),
const SizedBox(height: 24),
// Parameters
Text('Parameters', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
// Denoising steps slider
Row(
children: [
const Expanded(flex: 2, child: Text('Denoising Steps:')),
Expanded(
flex: 3,
child: Slider(
value: _totalSteps.toDouble(),
min: 1,
max: 20,
divisions: 19,
label: _totalSteps.toString(),
onChanged: _isLoading || _isGenerating
? null
: (value) =>
setState(() => _totalSteps = value.toInt()),
),
),
SizedBox(
width: 40,
child:
Text(_totalSteps.toString(), textAlign: TextAlign.right),
),
],
),
// Speed slider
Row(
children: [
const Expanded(flex: 2, child: Text('Speed:')),
Expanded(
flex: 3,
child: Slider(
value: _speed,
min: 0.5,
max: 2.0,
divisions: 30,
label: _speed.toStringAsFixed(2),
onChanged: _isLoading || _isGenerating
? null
: (value) => setState(() => _speed = value),
),
),
SizedBox(
width: 40,
child: Text(_speed.toStringAsFixed(2),
textAlign: TextAlign.right),
),
],
),
const SizedBox(height: 12),
// Language selector
Row(
children: [
const Expanded(flex: 2, child: Text('Language:')),
Expanded(
flex: 3,
child: DropdownButton<String>(
value: _selectedLang,
isExpanded: true,
items: const [
DropdownMenuItem(value: 'en', child: Text('English')),
DropdownMenuItem(value: 'ko', child: Text('한국어')),
DropdownMenuItem(value: 'es', child: Text('Español')),
DropdownMenuItem(value: 'pt', child: Text('Português')),
DropdownMenuItem(value: 'fr', child: Text('Français')),
],
onChanged: _isLoading || _isGenerating
? null
: (value) => setState(() => _selectedLang = value!),
),
),
],
),
const SizedBox(height: 24),
// Generate button
ElevatedButton.icon(
onPressed: _isLoading || _isGenerating
? null
: _isPlaying
? () async {
await _audioPlayer.stop();
setState(() => _status = 'Ready');
}
: _generateSpeech,
icon: Icon(_isPlaying ? Icons.stop : Icons.play_arrow),
label: Text(
_isGenerating
? 'Generating...'
: _isPlaying
? 'Stop Playback'
: 'Generate & Play Speech',
style: const TextStyle(fontSize: 16),
),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
// Download button
if (_lastGeneratedFilePath != null) ...[
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: _isLoading || _isGenerating ? null : _downloadFile,
icon: const Icon(Icons.download),
label: const Text('Download WAV File',
style: TextStyle(fontSize: 16)),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
],
],
),
),
);
}
@override
void dispose() {
_textController.dispose();
_audioPlayer.dispose();
super.dispose();
}
}