iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
AI Engineering

從行程到應援:30 天做出追星實用工具箱系列 第 18 篇

Day 18:直播結束一鍵生成精華摘要與逐字稿筆記功能實作

  • 分享至 

  • xImage
  •  

今日任務

Step 1: 建立摘要數據模型

在 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,
  });
}

Step 2: 實作一鍵摘要服務

在 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();
  }
}

Step 3: 實作一鍵複製與匯出工具

在 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();
  }
}

Step 4: UI 整合(「一鍵生成摘要」按鈕與彈窗)

在 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),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

檢查清單

  1. 在錄音過程中隨便講幾句偶像直播常見內容,例如:
    • 「今天 IVE的Rei來開直播了,他說:我的喉嚨還沒完全好,但請大家不要擔心,我會保護好自己。最近我們為了回歸忙得不可開交,希望 DIVE們會喜歡這張專輯!」
  2. 按下 「結束直播」 鈕。
  3. 彈窗自動觸發 LLM 摘要,幾秒內自動產出一份條理分明的精華筆記:
    • 主題:IVE Rei直播與回歸預告
    • 重點:叫大家別擔心自己、預告回歸計畫
    • 金句:「希望 DIVE們會喜歡這張專輯!」

上一篇
Day 17:上下文感知翻譯,避免口語化斷章取義
系列文
從行程到應援:30 天做出追星實用工具箱 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言