Logo
Published on

第一次學 Kotlin Koog AI 就上手 Day 12:讓 AI 聽懂聲音:音訊內容處理

在前一篇文章中,我們學會了如何讓 AI 看懂文件,現在讓我們繼續探索多模態的另一個重要面向:音訊處理。隨著語音助手和音訊內容的普及,讓 AI 能夠理解和處理音訊已成為現代應用的重要能力

本篇文章將帶您學習如何使用 Koog 處理音訊內容,包括音訊轉錄、內容分析,以及建立實用的會議記錄自動化系統

音訊處理基礎概念

支援的音訊格式

Koog 支援多種常見的音訊格式,確保您能處理各種來源的音訊檔案

  • MP3:最常見的音訊壓縮格式
  • WAV:無損音訊格式,品質最佳

支援音訊的 LLM 模型

目前主要支援音訊處理的模型是 OpenAI 的 GPT-4o (mini) Audio,這個模型專門針對音訊內容進行了優化,能夠

  • 準確轉錄各種語言的語音
  • 理解音訊中的情感和語調
  • 分析音訊內容並提供摘要
  • 識別多個說話者

音訊處理限制

使用音訊處理功能時需要注意以下限制

  • 檔案大小:單個音訊檔案不能超過 25MB
  • 時長限制:建議音訊長度在 20 分鐘以內,過長可能影響處理效果
  • 品質要求:清晰的錄音品質能顯著提升轉錄準確度

基礎音訊轉錄器

讓我們從一個簡單的音訊轉錄器開始

class AudioTranscriber(private val client: OpenAILLMClient) {

    /**
     * 判斷路徑是否為 URL
     */
    private fun isURL(path: String): Boolean {
        return path.startsWith("http://") || path.startsWith("https://")
    }

    /**
     * 轉錄音訊檔案內容
     * @param audioPath 音訊檔案路徑或 URL
     * @return 轉錄的文字內容
     */
    suspend fun transcribeAudio(audioPath: String): String {
        val response = client.execute(
            prompt = prompt("multimodel") {
                user {
                    text("請轉錄這段音訊的內容")
                    text("請提供準確的逐字轉錄,保持自然的句子結構,不要總結內容,直接給我音訊的逐字稿")

                    // 這是 Koog 的核心語法:在 attachments 區塊中處理音訊
                    attachments {
                        if (isURL(audioPath)) {
                            audio(audioPath) // 網路 URL 直接使用
                        } else {
                            audio(Path(audioPath)) // 本地檔案路徑需使用 Path
                        }
                    }
                }
            },
            model = OpenAIModels.Audio.GPT4oMiniAudio
        )

        return response.joinToString("") { it.content }
    }
}

基礎音訊轉錄器使用範例

// 使用範例
suspend fun main() {

    val client = OpenAILLMClient(ApiKeyManager.openAIApiKey!!)
    val transcriber = AudioTranscriber(client)

    println("=== Koog 音訊處理範例 ===\n")

    try {
        // 範例 1:處理本地音訊檔案
        println("1. 本地音訊檔案轉錄")
        val localTranscription = transcriber.transcribeAudio("/Users/cash/Downloads/podcast.mp3")
        println("轉錄結果:$localTranscription\n")

        // 範例 2:處理網路音訊檔案(如果有的話)
        // val urlTranscription = transcriber.transcribeAudio("https://example.com/audio.mp3")

    } catch (e: Exception) {
        println("轉錄失敗:${e.message}")
    }
}

執行 AI 回應內容

在這裡我是節錄 EP90|沈春華:「透過自我覺察,才能走出自己的路」,十三座金鐘獎紀錄保持人的智慧結晶,幫你在漫漫人生中淘金 對談開始的一小段音訊來測試,大約是 40 秒左右

=== Koog 音訊處理範例 ===

1. 本地音訊檔案轉錄
轉錄結果:這段音訊的逐字稿是:

「哇,今天真的是一個相當超現實的時刻,就是真的是我真的也是從小看『神劍』的節目到大的。因為我今年四十六歲。然後我小時候,我大姑最喜歡的節目就是《我愛紅娘》。你把你的長輩也帶出來了(對吧),因為可能大姑都已經是比我還年輕喔,搞不好。對,對,對。然後那時候就是永遠都記得那句台詞,然後永遠都記得說欸,怎麼辦,就是配對到底會不會成功,然後都相當的緊張。所以你是看《我愛紅娘》對不對?《我愛紅娘》、《紅娘愛我,為你搭起有情橋梁》。這個真的是大家耳熟能詳的一句slogan。」

可以看到有些字轉出來還是不太對,不過以結果來說,我覺得已經很不錯了

關鍵語法說明

在這個範例中,最重要的是 attachments 區塊配合 audio() 語法:

attachments {
    audio(Path(audioPath)) // 本地檔案使用 Path
    // 或
    audio(audioUrl)        // 網路 URL 直接使用
}

這與圖像處理(day10)和文件處理(day11)使用相同的模式,確保了 API 的一致性

進階功能:會議記錄自動化

現在讓我們實作一個更實用的功能「自動化會議記錄系統」,這個系統不僅能轉錄音訊,還能生成會議摘要和待辦事項

data class MeetingRecord(
    val transcription: String,
    val summary: String,
    val actionItems: List<String>
)

@Serializable
data class MeetingAnalysis(
    val summary: String,
    val actionItems: List<String> = emptyList()
)

class MeetingTranscriber(private val client: OpenAILLMClient) {

    /**
     * 處理會議錄音,生成完整的會議記錄
     */
    suspend fun processMeetingAudio(audioPath: String): MeetingRecord {
        // 第一步:轉錄音訊內容
        val transcription = transcribeWithSpeakers(audioPath)

        // 第二步:分析轉錄內容,生成摘要和待辦事項
        val analysis = analyzeMeetingContent(transcription)

        return analysis
    }

    /**
     * 轉錄音訊並識別說話者
     */
    private suspend fun transcribeWithSpeakers(audioPath: String): String {
        val response = client.execute(
            prompt = prompt("multimodel") {
                user {
                    text("""
                        請轉錄這段會議錄音的內容,並盡可能識別不同的說話者。
                        請使用以下格式:

                        [說話者A]: 說話內容
                        [說話者B]: 說話內容

                        如果無法區分說話者,可以使用 [說話者1]、[說話者2] 等標記。
                    """.trimIndent())

                    // 使用 attachments 區塊處理音訊
                    attachments {
                        if (isURL(audioPath)) {
                            audio(audioPath) // 網路 URL 直接使用
                        } else {
                            audio(Path(audioPath)) // 本地檔案路徑需使用 Path
                        }
                    }
                }
            },
            model = OpenAIModels.Audio.GPT4oMiniAudio
        )

        return response.joinToString { it.content }
    }

    /**
     * 判斷路徑是否為 URL
     */
    private fun isURL(path: String): Boolean {
        return path.startsWith("http://") || path.startsWith("https://")
    }

    /**
     * 分析會議內容,生成摘要和待辦事項
     */
    private suspend fun analyzeMeetingContent(transcription: String): MeetingRecord {
        val response = client.execute(
            prompt = prompt {
                user {
                    text("""
                        請分析以下會議轉錄內容
                        `$transcription`

                        請先幫我總結相關的「會議摘要」(3-5句話),和列出相關的「待辦事項」(如果有的話)
                        並且請使用以下的 JSON 格式回答
                        {
                            "summary": "會議摘要",
                            "actionItems": ["待辦事項1", "待辦事項2"]
                        }
                    """.trimIndent())
                }
            },
            model = OpenAIModels.CostOptimized.GPT4_1Mini
        )

        // 使用 kotlinx-serialization-json 解析 AI 回應
        val fullContent = response.joinToString("") { it.content }

        // 清理 Markdown 程式碼區塊標記
        val cleanedContent = fullContent
            .replace("```json", "")
            .replace("```", "")
            .trim()

        return try {
            val analysis = Json.decodeFromString<MeetingAnalysis>(cleanedContent)
            MeetingRecord(
                transcription = transcription,
                summary = analysis.summary,
                actionItems = analysis.actionItems
            )
        } catch (e: Exception) {
            // 如果 JSON 解析失敗,回退到預設值
            MeetingRecord(
                transcription = transcription,
                summary = "解析會議摘要時發生錯誤",
                actionItems = emptyList()
            )
        }
    }
}

會議記錄自動化使用範例

suspend fun main() {
    val client = OpenAILLMClient(ApiKeyManager.openAIApiKey!!)
    val meetingTranscriber = MeetingTranscriber(client)

    println("=== 會議記錄自動化範例 ===\n")

    try {
        val meetingRecord = meetingTranscriber.processMeetingAudio("/Users/cash/Downloads/podcast.mp3")

        println("=== 會議記錄 ===")

        println("原始對話:\n${meetingRecord.transcription}")
        println("\n摘要:${meetingRecord.summary}")
        println("\n待辦事項:")
        meetingRecord.actionItems.forEach { item ->
            println("- $item")
        }

    } catch (e: Exception) {
        println("處理會議錄音失敗:${e.message}")
    }
}

執行 AI 回應內容

因為沒有會議記錄可以拿來測試,所以就拿第一個 Podcast 的音訊來測試

=== 會議記錄自動化範例 ===

=== 會議記錄 ===
原始對話:
[說話者A]: 哇,今天真的是一個相當超現實的一刻。就是真的是,我真的是從小看《神奇》的節目到大的。因為我今年46歲。
[說話者B]: 然後我小時候我大姑最喜歡的節目就是《我愛紅娘》。
[說話者A]: 你把你的長輩也拉出來。
[說話者B]: 對,我真的是從小看《我愛紅娘》長大的。你當然是從小看我長大的,對不對?因為可能大姑都已經是比我還年輕,搞不好。
[說話者A]: 對,對,對。然後那時候就是永遠都記得那句台詞。
[說話者B]: 永遠都記得說,欸,怎麼辦,就是配對到底會不會成功,然後都相當緊張。
[說話者A]: 喔,所以你是看《我愛紅娘》。
[說話者B]: 對,《我愛紅娘》,《紅娘愛我》。
[說話者A]: 為你搭起有情的橋樑。
[說話者B]: 喔,這個真的是大家耳熟能詳的一句slogan。

摘要:這段會議轉錄提到兩位參與者分享了他們從小喜愛的電視節目。說話者A提到了《神節》和其標誌性的台詞,並表達了他從小到大對這些節目的情感連結。說話者B則談到了與《我愛鳳凰》相關的節目,以及這些節目中令人印象深刻的slogan。

待辦事項:

注意事項

在本篇的範例中,為了簡單方便,我們並沒有考慮到使用者上傳音訊時可能遇到的各種錯誤情況。在正式的程式碼中,建議一併考慮並實作以下錯誤處理

  • 檔案驗證:檢查音訊檔案是否存在
  • 格式檢查:確認檔案格式是否支援(mp3, wav, m4a, flac 等)
  • 大小限制:確保檔案大小未超過 API 限制(25MB)
  • 時長檢查:建議音訊長度在 20 分鐘以內
  • 網路問題:處理網路音訊載入失敗的情況
  • API 調用錯誤:處理 AI 服務的回應錯誤

音訊處理成本考量

使用 OpenAI 的音訊處理功能需要特別注意成本問題,相較於純文字處理,音訊處理的費用相對較高

OpenAI 音訊處理定價(寫文章當下的價錢)

  • GPT-4o
    • $40.00 / 1M input tokens
    • $80.00 / 1M output tokens
  • GPT-4o mini
    • $10.00 / 1M input tokens
    • $20.00 / 1M output tokens

在撰寫這篇文章的過程中,僅是反覆測試和調整範例程式碼,就花費了約 0.5 美元

進階音訊處理:使用 Attachment API

除了前面介紹的簡化語法,Koog 還提供了更靈活的 Attachment API,讓我們能夠更精確地控制音訊處理過程。這種方式特別適合需要處理不同音訊來源或需要更多控制參數的場景

AttachmentContent 類型說明

Koog 提供了多種內容來源類型,讓我們能夠靈活處理不同的音訊來源

AttachmentContent.URL

// 直接從網路 URL 載入音訊
AttachmentContent.URL("https://example.com/audio.mp3")

AttachmentContent.Binary.Bytes

// 從本地檔案讀取為 byte array
val audioBytes = Files.readAllBytes(Path("/path/to/audio.mp3"))
AttachmentContent.Binary.Bytes(audioBytes)

兩種方法的比較

功能特色簡化語法 (audio())Attachment API
使用難度簡單直觀稍微複雜
參數控制基本完整控制
音訊來源URL + 本地檔案URL + Bytes + Base64
批次處理需要多次調用原生支援
檔案資訊自動推斷可指定格式、檔名等
適用場景快速原型開發生產環境應用

使用建議

  • 快速原型:使用簡化的 audio() 語法
  • 生產應用:使用 Attachment API 以獲得更好的控制
  • 批次處理:優先考慮 Attachment API 的批次功能
  • 網頁整合:Base64 支援對於網頁上傳的音訊很有用

總結

在這篇文章中,我們學習了

  • 音訊處理基礎:了解 Koog 支援的音訊格式和模型
  • 基本轉錄功能:使用 attachments 區塊配合 audio() 語法處理音訊檔案
  • 會議記錄自動化:實作完整的會議錄音分析系統
  • 進階 Attachment API:透過結構化的 API 處理不同音訊來源(URL、Bytes、Base64)
  • 兩種方法比較:了解何時使用簡化語法或 Attachment API

音訊處理為我們的 AI 應用開啟了新的可能性,從簡單的語音轉錄到複雜的會議分析,都能透過 Koog 輕鬆實現。掌握了圖像處理(day10)、文件處理(day11)和音訊處理(day12)後,我們已經具備了完整的多媒體 AI 處理能力

下一篇文章,我們將學習如何使用快取機制來提升應用效能,避免重複處理相同的內容

參考資料


支持創作

如果這篇文章對您有幫助,歡迎透過 贊助連結 支持我持續創作優質內容。您的支持是我前進的動力!


圖片來源:AI 產生