在 lib/features/live_translate/data/models/ 建立 summary_model.dart
class LiveSummaryResult {
final String title; // 直播/會議主題
final List<String> keyPoints; // 核心重點列表
final List<String> highlights; // 精華金句/精彩片段
final String markdownContent; // 完整的 Markdown 筆記內容
LiveSummaryResult({
required this.title,
required this.keyPoints,
required this.highlights,
required this.markdownContent,
});
}
在 lib/features/live_translate/data/services/ 建立 summary_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../models/summary_model.dart';
class SummaryService {
final String _openAiUrl = 'https://api.openai.com/v1/chat/completions';
/// 將整場直播/對話的逐字稿發送給 LLM,生成結構化精華筆記
Future<LiveSummaryResult?> generateSummary({
required List<String> fullTranscripts, // 整場累積的逐字稿清單
String targetLanguage = '繁體中文',
}) async {
if (fullTranscripts.isEmpty) return null;
final apiKey = dotenv.env['OPENAI_API_KEY'];
if (apiKey == null || apiKey.isEmpty) return null;
// 將逐字稿合併為帶有編號的文本
final combinedText = fullTranscripts.asMap().entries.map((entry) {
return '[片段 ${entry.key + 1}]: ${entry.value}';
}).join('\n');
try {
final response = await http.post(
Uri.parse(_openAiUrl),
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Authorization': 'Bearer $apiKey',
},
body: jsonEncode({
'model': 'gpt-4o-mini',
'temperature': 0.4,
'messages': [
{
'role': 'system',
'content': '''你是一位專業的內容分析師與速記專家。請根據提供給你的直播/語音記錄逐字稿,用 $targetLanguage 生成結構化的 Markdown 精華筆記。
請嚴格按照以下格式輸出:
# 直播精華主題:[請寫出一個簡短扼要的主題名稱]
## 核心重點摘要
- [重點 1]
- [重點 2]
- [重點 3]
## 重點
- 「[精彩發言或經典台詞 1]」
- 「[精彩發言或經典台詞 2]」
## 詳細內容紀錄
[請將內容進行結構化的整理與邏輯重組,修飾口語為書面語]'''
},
{
'role': 'user',
'content': '以下是整場直播的完整逐字稿記錄:\n\n$combinedText',
}
],
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(utf8.decode(response.bodyBytes));
final String rawMarkdown = data['choices'][0]['message']['content'] ?? '';
return LiveSummaryResult(
title: _extractTitle(rawMarkdown),
keyPoints: [],
highlights: [],
markdownContent: rawMarkdown,
);
} else {
print('生成摘要失敗: ${response.body}');
return null;
}
} catch (e) {
print('SummaryService 異常: $e');
return null;
}
}
String _extractTitle(String markdown) {
final firstLine = markdown.split('\n').firstWhere(
(line) => line.startsWith('# '),
orElse: () => '# 直播筆記摘要',
);
return firstLine.replaceAll('# ', '').trim();
}
}
在 lib/core/utils/ 建立 transcript_export_utils.dart(或是放在 live_translate/utils/)
import 'package:flutter/services.dart';
class TranscriptExportUtils {
/// 一鍵複製文本到剪貼簿
static Future<void> copyToClipboard(String text) async {
await Clipboard.setData(ClipboardData(text: text));
}
/// 格式化匯出文字檔內容(包含時間與逐字稿列表)
static String formatFullExport({
required String title,
required List<String> rawTranscripts,
required List<String> translatedTexts,
}) {
final buffer = StringBuffer();
buffer.writeln('====================================');
buffer.writeln('Live Translate 逐字稿與翻譯紀錄');
buffer.writeln('匯出時間: ${DateTime.now().toString().substring(0, 19)}');
buffer.writeln('====================================\n');
for (int i = 0; i < rawTranscripts.length; i++) {
buffer.writeln('[$i] 原文: ${rawTranscripts[i]}');
if (i < translatedTexts.length) {
buffer.writeln(' 譯文: ${translatedTexts[i]}');
}
buffer.writeln('------------------------------------');
}
return buffer.toString();
}
}
在 multi_lang_page.dart (或你的 Live Translate 主頁面) 加入直播結束時的摘要彈窗
final SummaryService _summaryService = SummaryService();
List<String> _recordedSessionTranscripts = []; // 儲存這場直播錄到的所有逐字稿
void _onFinishLiveStream(BuildContext context) async {
if (_recordedSessionTranscripts.isEmpty) return;
// 顯示 Loading 提示
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => const Center(
child: Card(
child: Padding(
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 15),
Text('正在精煉直播重點與筆記中...'),
],
),
),
),
),
);
// 呼叫 LLM 產生摘要
final summary = await _summaryService.generateSummary(
fullTranscripts: _recordedSessionTranscripts,
);
Navigator.pop(context); // 關閉 Loading
if (summary != null) {
// 顯示精華摘要 Modal
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (ctx) => Container(
padding: const EdgeInsets.all(20),
height: MediaQuery.of(context).size.height * 0.75,
child: Column(
crossAxisAlignment: CrossAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('筆記精華', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
IconButton(
icon: const Icon(Icons.copy),
onPressed: () {
TranscriptExportUtils.copyToClipboard(summary.markdownContent);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已複製精華筆記至剪貼簿!')),
);
},
),
],
),
const Divider(),
Expanded(
child: SingleChildScrollView(
child: Text(
summary.markdownContent,
style: const TextStyle(fontSize: 16, height: 1.5),
),
),
),
],
),
),
);
}
}