import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart'; // Your deployed function URL, e.g. https://assess-xxxx-uc.a.run.app const functionUrl = String.fromEnvironment('ASSESS_URL'); const sentence = {'text': '我想买书', 'language': 'zh-CN'}; Future main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); // run `flutterfire configure` first await FirebaseAuth.instance.signInAnonymously(); runApp(const MaterialApp(home: ScorePage())); } class ScorePage extends StatefulWidget { const ScorePage({super.key}); @override State createState() => _ScorePageState(); } class _ScorePageState extends State { final _recorder = AudioRecorder(); bool _recording = false, _busy = false; Map? _result; Future _toggle() async { if (!_recording) { if (!await _recorder.hasPermission()) return; final dir = await getTemporaryDirectory(); await _recorder.start(const RecordConfig(encoder: AudioEncoder.aacLc), path: '${dir.path}/take.m4a'); setState(() => _recording = true); return; } final path = await _recorder.stop(); setState(() { _recording = false; _busy = true; }); try { final token = await FirebaseAuth.instance.currentUser!.getIdToken(); final response = await http.post(Uri.parse(functionUrl), headers: { 'Authorization': 'Bearer $token', 'Content-Type': 'application/json', 'Idempotency-Key': 'take-${DateTime.now().microsecondsSinceEpoch}-${Random().nextInt(1 << 30)}', }, body: jsonEncode({...sentence, 'audio_base64': base64Encode(await File(path!).readAsBytes())})); setState(() => _result = jsonDecode(utf8.decode(response.bodyBytes)) as Map); } catch (e) { setState(() => _result = {'error': {'message': 'Network error: $e'}}); } finally { setState(() => _busy = false); } } @override Widget build(BuildContext context) { final r = _result; final words = (r?['words'] as List?) ?? const []; return Scaffold( appBar: AppBar(title: Text('Say: ${sentence['text']}')), body: ListView(padding: const EdgeInsets.all(24), children: [ FilledButton(onPressed: _busy ? null : _toggle, child: Text(_recording ? 'Stop' : _busy ? 'Scoring…' : 'Record')), if (r?['error'] != null) Text('${r!['error']['message']}'), if (r != null && r['error'] == null && r['assessable'] == false) Text('Could not score that recording (${r['reason']}).'), for (final w in words) ListTile( title: Text('${w['text'] ?? ''} ${w['pinyin'] ?? ''}'), subtitle: Text(() { final tone = (w['units'] as List).cast().where((u) => u['dimension'] == 'tone'); return tone.isEmpty ? '' : 'tone ${tone.first['expected']} → ${tone.first['heard'] ?? '?'}'; }()), trailing: Text('${w['score'] ?? '—'}'), ), if (r?['test'] == true) const Text('Test key: fixed example result.'), ]), ); } }