mymuseum-visitapp/lib/Services/Glasses/engines/impl/whisper_stt_engine.dart

108 lines
3.3 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:mymuseum_visitapp/Services/Glasses/engines/stt_engine.dart';
/// STT via API Whisper — format OpenAI multipart/form-data.
///
/// Compatible avec :
/// - OpenAI Whisper API : endpoint = 'https://api.openai.com/v1/audio/transcriptions'
/// - faster-whisper-server : endpoint = 'http://ton-serveur:8000/v1/audio/transcriptions'
/// - openedai-speech : même format
///
/// Coût : $0.006/min (OpenAI) ou gratuit si auto-hébergé.
class WhisperSttEngine implements SttEngine {
final String endpoint;
final String apiKey;
final String model;
final FlutterSoundRecorder _recorder = FlutterSoundRecorder();
bool _initialized = false;
String? _currentPath;
WhisperSttEngine({
required this.apiKey,
this.endpoint = 'https://api.openai.com/v1/audio/transcriptions',
this.model = 'whisper-1',
});
Future<void> _ensureInitialized() async {
if (_initialized) return;
await Permission.microphone.request();
await _recorder.openRecorder();
_initialized = true;
}
@override
Future<String> transcribeOnce({
required String languageCode,
Duration timeout = const Duration(seconds: 8),
}) async {
await _ensureInitialized();
final dir = await getTemporaryDirectory();
_currentPath = '${dir.path}/whisper_stt_${DateTime.now().millisecondsSinceEpoch}.wav';
try {
await _recorder.startRecorder(
toFile: _currentPath,
codec: Codec.pcm16WAV,
sampleRate: 16000,
numChannels: 1,
);
await Future.delayed(timeout);
await _recorder.stopRecorder();
return await _transcribe(_currentPath!, languageCode);
} catch (e) {
debugPrint('[WhisperSttEngine] error: $e');
try { await _recorder.stopRecorder(); } catch (_) {}
return '';
} finally {
try {
if (_currentPath != null) File(_currentPath!).deleteSync();
} catch (_) {}
_currentPath = null;
}
}
@override
Future<void> cancel() async {
try { await _recorder.stopRecorder(); } catch (_) {}
}
Future<String> _transcribe(String wavPath, String languageCode) async {
final lang = languageCode.split('-').first; // "fr-FR" → "fr"
final bytes = await File(wavPath).readAsBytes();
final request = http.MultipartRequest('POST', Uri.parse(endpoint))
..headers['Authorization'] = 'Bearer $apiKey'
..fields['model'] = model
..fields['language'] = lang
..fields['response_format'] = 'json'
..files.add(http.MultipartFile.fromBytes(
'file',
bytes,
filename: 'audio.wav',
));
final response = await request.send().timeout(const Duration(seconds: 15));
final body = await response.stream.bytesToString();
if (response.statusCode != 200) {
debugPrint('[WhisperSttEngine] HTTP ${response.statusCode}: $body');
return '';
}
final json = jsonDecode(body) as Map<String, dynamic>;
final text = (json['text'] as String? ?? '').trim();
debugPrint('[WhisperSttEngine] "$text"');
return text;
}
}