diff --git a/Sources/VoiceMemo/AudioRecorder.swift b/Sources/VoiceMemo/AudioRecorder.swift index 4000db5..ed08c51 100644 --- a/Sources/VoiceMemo/AudioRecorder.swift +++ b/Sources/VoiceMemo/AudioRecorder.swift @@ -407,6 +407,7 @@ class AudioRecorder: NSObject, ObservableObject, SCStreamOutput, SCStreamDelegat let dateStr = formatter.string(from: self.recordingStartTime ?? Date()) let title = "Rec \(dateStr)" let task = MeetingTask(recordingId: recId, localFilePath: mixedURL.path, title: title) + task.rawLocalFilePath = mixedURL.path Task { try? await StorageManager.shared.currentProvider.saveTask(task) } self.latestTask = task } @@ -430,6 +431,7 @@ class AudioRecorder: NSObject, ObservableObject, SCStreamOutput, SCStreamDelegat let dateStr = formatter.string(from: self.recordingStartTime ?? Date()) let title = "Rec \(dateStr)" let task = MeetingTask(recordingId: recId, localFilePath: finalURL.path, title: title) + task.rawLocalFilePath = finalURL.path Task { try? await StorageManager.shared.currentProvider.saveTask(task) } self.latestTask = task } diff --git a/Sources/VoiceMemo/Models/MeetingTask.swift b/Sources/VoiceMemo/Models/MeetingTask.swift index 757f192..30de57e 100644 --- a/Sources/VoiceMemo/Models/MeetingTask.swift +++ b/Sources/VoiceMemo/Models/MeetingTask.swift @@ -75,23 +75,21 @@ class MeetingTask: Identifiable, ObservableObject, Hashable, Codable { // File Paths var localFilePath: String // Mixed audio path + var rawLocalFilePath: String? // Original local audio path var originalOssUrl: String? // Mixed Original OSS URL var ossUrl: String? // Mixed OSS URL - // Tingwu Info - var tingwuTaskId: String? // Mixed Task ID + // Transcription Info + var transcriptionTaskId: String? var taskKey: String? - var apiStatus: String? - var statusText: String? var bizDuration: Int? var status: MeetingTaskStatus = .recorded var title: String // Results - var rawResponse: String? var transcript: String? var summary: String? @@ -108,7 +106,6 @@ class MeetingTask: Identifiable, ObservableObject, Hashable, Codable { // Complete Poll Results Storage var overviewData: String? var transcriptData: String? - var conversationData: String? var rawData: String? init(recordingId: String, localFilePath: String, title: String) { @@ -144,7 +141,7 @@ extension MeetingTask { // Fallback: Check if we have specific fields that might hint // This is a weak heuristic but better than nothing for legacy data - if let _ = self.tingwuTaskId { + if let _ = self.transcriptionTaskId { // Both use this field currently, so it's not decisive unless we check format // But if we have no raw data, we might assume Tingwu as it was the default/first return "Unknown" @@ -169,8 +166,6 @@ extension MeetingTask { md += "## Task Info\n" if let key = taskKey { md += "- Task Key: \(key)\n" } - if let status = apiStatus { md += "- Status: \(status)\n" } - if let error = statusText, !error.isEmpty { md += "- Message: \(error)\n" } if let duration = bizDuration { md += "- Duration: \(duration / 1000)s\n" } if let mp3 = outputMp3Path { md += "- Audio: [Download](\(mp3))\n" } md += "\n" @@ -198,7 +193,7 @@ extension MeetingTask { if let transcript = transcript, !transcript.isEmpty { return transcript } - + if let dataStr = transcriptData, let data = dataStr.data(using: .utf8), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { @@ -206,14 +201,8 @@ extension MeetingTask { return text } } - - guard let raw = rawResponse, - let data = raw.data(using: .utf8), - let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - return nil - } - - return TranscriptParser.buildTranscriptText(from: json) + + return nil } func safeFilename() -> String { diff --git a/Sources/VoiceMemo/Services/HistoryStore.swift b/Sources/VoiceMemo/Services/HistoryStore.swift index 376b39a..603d95f 100644 --- a/Sources/VoiceMemo/Services/HistoryStore.swift +++ b/Sources/VoiceMemo/Services/HistoryStore.swift @@ -133,6 +133,7 @@ class HistoryStore: ObservableObject { let dst = try copyFile(from: url, suffix: "") task.localFilePath = dst.path + task.rawLocalFilePath = dst.path task.title = url.deletingPathExtension().lastPathComponent try await StorageManager.shared.currentProvider.saveTask(task) diff --git a/Sources/VoiceMemo/Services/MeetingPipelineManager.swift b/Sources/VoiceMemo/Services/MeetingPipelineManager.swift index 22d6d30..3a75123 100644 --- a/Sources/VoiceMemo/Services/MeetingPipelineManager.swift +++ b/Sources/VoiceMemo/Services/MeetingPipelineManager.swift @@ -75,7 +75,7 @@ class MeetingPipelineManager: ObservableObject { resetTask.status = .recorded resetTask.originalOssUrl = nil resetTask.ossUrl = nil - resetTask.tingwuTaskId = nil + resetTask.transcriptionTaskId = nil resetTask.transcript = nil resetTask.summary = nil resetTask.failedStep = nil @@ -201,7 +201,18 @@ class MeetingPipelineManager: ObservableObject { // Prepare summary attachment if settings.emailAttachSummary { - let mdContent = task.markdownSummary() + var mdContent = task.markdownSummary() + if settings.emailAttachAudio { + var links: [String] = [] + if let url = task.ossUrl { links.append("- Processed: \(url)") } + if let url = task.originalOssUrl { links.append("- Original: \(url)") } + if let url = task.outputMp3Path { links.append("- MP3: \(url)") } + if !links.isEmpty { + mdContent += "## Audio Links\n" + mdContent += links.joined(separator: "\n") + mdContent += "\n\n" + } + } let mdFilename = baseFilename.appending(".md") let mdUrl = FileManager.default.temporaryDirectory.appendingPathComponent(mdFilename) if let mdData = mdContent.data(using: .utf8) { @@ -211,21 +222,6 @@ class MeetingPipelineManager: ObservableObject { } } - // Prepare audio attachment - if settings.emailAttachAudio { - if FileManager.default.fileExists(atPath: task.localFilePath) { - attachmentPaths.append(task.localFilePath) - } else if let mp3UrlString = task.outputMp3Path, let mp3Url = URL(string: mp3UrlString) { - // Try to download from URL - if let (tempUrl, _) = try? await URLSession.shared.download(from: mp3Url) { - let destination = FileManager.default.temporaryDirectory.appendingPathComponent(mp3Url.lastPathComponent) - try? FileManager.default.moveItem(at: tempUrl, to: destination) - tempFiles.append(destination) - attachmentPaths.append(destination.path) - } - } - } - // Prepare transcript attachment if settings.emailAttachTranscript, let transcriptText = task.derivedTranscriptText() { let transcriptFilename = baseFilename.appending("-transcript.txt") @@ -239,7 +235,7 @@ class MeetingPipelineManager: ObservableObject { // Prepare raw data attachment if settings.emailAttachRawData { - if let rawDataStr = task.rawData ?? task.rawResponse { + if let rawDataStr = task.rawData { let rawFilename = baseFilename.appending("-raw.json") let rawUrl = FileManager.default.temporaryDirectory.appendingPathComponent(rawFilename) if let rawData = rawDataStr.data(using: .utf8) { @@ -297,30 +293,55 @@ class MeetingPipelineManager: ObservableObject { // Hydrate Mixed Channel (0) var mixed = ChannelData() - // Fix: Resolve processed path if missing + if let rawPath = task.rawLocalFilePath, !rawPath.isEmpty { + mixed.rawAudioPath = rawPath + } + let path = task.localFilePath if !path.isEmpty { if path.hasSuffix("_48k.m4a") { mixed.processedAudioPath = path - mixed.rawAudioPath = path // Fallback - } else { + } else if mixed.rawAudioPath == nil { mixed.rawAudioPath = path - // Check for existing processed file - let url = URL(fileURLWithPath: path) - let processedFilename = "mixed_48k.m4a" - let processedUrl = url.deletingLastPathComponent().appendingPathComponent(processedFilename) - if FileManager.default.fileExists(atPath: processedUrl.path) { - mixed.processedAudioPath = processedUrl.path + } + } + + if let rawPath = mixed.rawAudioPath, !rawPath.isEmpty, mixed.processedAudioPath == nil { + let url = URL(fileURLWithPath: rawPath) + let inputFilename = url.deletingPathExtension().lastPathComponent + + let formatter = DateFormatter() + formatter.dateFormat = "yyyyMMdd-HHmmss" + let timestamp = formatter.string(from: task.createdAt) + + let timestampedFilename: String + if inputFilename.contains(timestamp) { + timestampedFilename = "\(inputFilename)_48k.m4a" + } else { + timestampedFilename = "\(inputFilename)-\(timestamp)_48k.m4a" + } + let timestampedUrl = url.deletingLastPathComponent().appendingPathComponent(timestampedFilename) + + let newProcessedFilename = "\(inputFilename)_48k.m4a" + let newProcessedUrl = url.deletingLastPathComponent().appendingPathComponent(newProcessedFilename) + + if FileManager.default.fileExists(atPath: timestampedUrl.path) { + mixed.processedAudioPath = timestampedUrl.path + } else if FileManager.default.fileExists(atPath: newProcessedUrl.path) { + mixed.processedAudioPath = newProcessedUrl.path + } else { + let legacyProcessedFilename = "mixed_48k.m4a" + let legacyProcessedUrl = url.deletingLastPathComponent().appendingPathComponent(legacyProcessedFilename) + if FileManager.default.fileExists(atPath: legacyProcessedUrl.path) { + mixed.processedAudioPath = legacyProcessedUrl.path } } } mixed.rawAudioOssURL = task.originalOssUrl mixed.processedAudioOssURL = task.ossUrl - mixed.tingwuTaskId = task.tingwuTaskId + mixed.transcriptionTaskId = task.transcriptionTaskId mixed.taskKey = task.taskKey - mixed.apiStatus = task.apiStatus - mixed.statusText = task.statusText mixed.bizDuration = task.bizDuration board.channels[0] = mixed @@ -340,12 +361,11 @@ class MeetingPipelineManager: ObservableObject { private func updateChannelFields(channel: ChannelData) { if let url = channel.rawAudioOssURL { self.task.originalOssUrl = url } + if let path = channel.rawAudioPath { self.task.rawLocalFilePath = path } if let path = channel.processedAudioPath { self.task.localFilePath = path } if let url = channel.processedAudioOssURL { self.task.ossUrl = url } - if let tid = channel.tingwuTaskId { self.task.tingwuTaskId = tid } + if let tid = channel.transcriptionTaskId { self.task.transcriptionTaskId = tid } if let key = channel.taskKey { self.task.taskKey = key } - if let status = channel.apiStatus { self.task.apiStatus = status } - if let text = channel.statusText { self.task.statusText = text } if let dur = channel.bizDuration { self.task.bizDuration = dur } if let res = channel.transcript { self.task.transcript = res.text @@ -395,7 +415,6 @@ class MeetingPipelineManager: ObservableObject { } } if let transcript = channel.transcriptData { self.task.transcriptData = transcript } - if let conversation = channel.conversationData { self.task.conversationData = conversation } if let raw = channel.rawData { self.task.rawData = raw } } diff --git a/Sources/VoiceMemo/Services/Pipeline/PipelineBoard.swift b/Sources/VoiceMemo/Services/Pipeline/PipelineBoard.swift index 611f338..24b44f0 100644 --- a/Sources/VoiceMemo/Services/Pipeline/PipelineBoard.swift +++ b/Sources/VoiceMemo/Services/Pipeline/PipelineBoard.swift @@ -47,14 +47,12 @@ struct ChannelData { var processedAudioPath: String? // 本地路径 (如 16k/48k m4a) var processedAudioOssURL: String? // OSS URL (用于提交给听悟) - // 3. 听悟任务 - var tingwuTaskId: String? - var tingwuTaskStatus: String? // e.g. "RUNNING", "COMPLETE" + // 3. Transcription Task + var transcriptionTaskId: String? + var transcriptionTaskStatus: String? // e.g. "RUNNING", "COMPLETE" // Additional Task Info var taskKey: String? - var apiStatus: String? - var statusText: String? var bizDuration: Int? // 4. 最终产物 @@ -63,7 +61,6 @@ struct ChannelData { // 5. 完整轮询结果 (用于数据库存储) var overviewData: String? // 概览数据 (JSON String) var transcriptData: String? // 转录数据 (JSON String) - var conversationData: String? // 对话数据 (JSON String) var rawData: String? // 原始数据 (JSON String) // 6. 错误追踪 (用于单路重试) diff --git a/Sources/VoiceMemo/Services/Pipeline/PipelineNodes.swift b/Sources/VoiceMemo/Services/Pipeline/PipelineNodes.swift index 28cb2da..fa27a2e 100644 --- a/Sources/VoiceMemo/Services/Pipeline/PipelineNodes.swift +++ b/Sources/VoiceMemo/Services/Pipeline/PipelineNodes.swift @@ -50,7 +50,14 @@ class UploadOriginalNode: PipelineNode { let datePath = board.formattedDatePath() let fileURL = URL(fileURLWithPath: inputPath) - let filename = channelId == 0 ? "mixed_raw.m4a" : "speaker\(channelId)_raw.m4a" + + // Fix: Use input filename to ensure uniqueness (e.g. includes UUID/timestamp) + var inputFilename = fileURL.deletingPathExtension().lastPathComponent + if !inputFilename.hasSuffix("_raw") { + inputFilename += "_raw" + } + let filename = "\(inputFilename).m4a" + let objectKey = "\(board.config.ossPrefix)\(datePath)/\(board.recordingId)/\(filename)" let url = try await services.ossService.uploadFile(fileURL: fileURL, objectKey: objectKey) @@ -76,14 +83,34 @@ class TranscodeNode: PipelineNode { // 2. Prepare Output Path let inputURL = URL(fileURLWithPath: inputPath) - let outputFilename = channelId == 0 ? "mixed_48k.m4a" : "speaker\(channelId)_48k.m4a" + + // Fix: Use input filename as base + timestamp to ensure uniqueness and avoid collisions + var inputFilename = inputURL.deletingPathExtension().lastPathComponent + + // Avoid recursive suffixing if re-running on already processed file + if inputFilename.hasSuffix("_48k") { + inputFilename = String(inputFilename.dropLast(4)) + } + + // Append timestamp from task creation date to ensure global uniqueness per task + let formatter = DateFormatter() + formatter.dateFormat = "yyyyMMdd-HHmmss" + let timestamp = formatter.string(from: board.creationDate) + + // If filename already contains this timestamp, don't append it again + let outputFilename: String + if inputFilename.contains(timestamp) { + outputFilename = "\(inputFilename)_48k.m4a" + } else { + outputFilename = "\(inputFilename)-\(timestamp)_48k.m4a" + } + let outputURL = inputURL.deletingLastPathComponent().appendingPathComponent(outputFilename) - // 3. Idempotency Check + // 3. Force Overwrite (User requirement: "If file exists, overwrite it") if FileManager.default.fileExists(atPath: outputURL.path) { - print("TranscodeNode: Processed file exists, skipping transcode.") - board.updateChannel(channelId) { $0.processedAudioPath = outputURL.path } - return + print("TranscodeNode: Output file exists, deleting to overwrite: \(outputURL.path)") + try? FileManager.default.removeItem(at: outputURL) } // 4. Execution (Always transcode for now to ensure quality) @@ -136,8 +163,14 @@ class UploadNode: PipelineNode { let datePath = board.formattedDatePath() let fileURL = URL(fileURLWithPath: inputPath) - // Note: Keeping legacy naming convention (mixed.m4a instead of mixed_48k.m4a in OSS) for consistency with existing data - let filename = channelId == 0 ? "mixed.m4a" : "speaker\(channelId).m4a" + + // Use input filename logic to ensure uniqueness and correct extension + let inputFilename = fileURL.deletingPathExtension().lastPathComponent + // Remove "_48k" suffix if present to avoid duplication in OSS key if desired, + // OR just keep it as is. User asked for uniqueness like local/remote. + // Let's use the file's actual name to be safe and unique. + let filename = "\(inputFilename).m4a" + let objectKey = "\(board.config.ossPrefix)\(datePath)/\(board.recordingId)/\(filename)" let url = try await services.ossService.uploadFile(fileURL: fileURL, objectKey: objectKey) @@ -157,12 +190,21 @@ class CreateTaskNode: PipelineNode { func run(board: inout PipelineBoard, services: ServiceProvider) async throws { // 1. Read Input let channel = try board.getChannel(channelId) - guard let url = channel.processedAudioOssURL else { - throw PipelineError.inputMissing("OSS URL missing") + + // 优先使用转码后的音频 URL (Processed),如果不存在则使用原始音频 URL (Raw) + // 听悟建议使用 16k/48k 采样率的 m4a/mp3,所以优先用转码后的 + let url: String + if let processedUrl = channel.processedAudioOssURL { + url = processedUrl + } else if let rawUrl = channel.rawAudioOssURL { + print("CreateTaskNode: Processed URL missing, falling back to Raw URL.") + url = rawUrl + } else { + throw PipelineError.inputMissing("Both Processed and Raw OSS URLs are missing") } // 2. Idempotency Check - if channel.tingwuTaskId != nil { + if channel.transcriptionTaskId != nil { print("CreateTaskNode: Task ID exists, skipping creation.") return } @@ -172,7 +214,7 @@ class CreateTaskNode: PipelineNode { let taskId = try await services.transcriptionService.createTask(fileUrl: url) // 4. Write Output - board.updateChannel(channelId) { $0.tingwuTaskId = taskId } + board.updateChannel(channelId) { $0.transcriptionTaskId = taskId } } } @@ -186,7 +228,7 @@ class PollingNode: PipelineNode { func run(board: inout PipelineBoard, services: ServiceProvider) async throws { // 1. Read Input let channel = try board.getChannel(channelId) - guard let taskId = channel.tingwuTaskId else { + guard let taskId = channel.transcriptionTaskId else { throw PipelineError.inputMissing("Task ID missing") } @@ -194,28 +236,23 @@ class PollingNode: PipelineNode { let (status, data) = try await services.transcriptionService.getTaskInfo(taskId: taskId) // 3. Write Status - board.updateChannel(channelId) { - $0.tingwuTaskStatus = status - $0.apiStatus = status - + board.updateChannel(channelId) { + $0.transcriptionTaskStatus = status + if let data = data { // Volcengine: audio_info.duration if let audioInfo = data["audio_info"] as? [String: Any], let duration = audioInfo["duration"] as? Int { $0.bizDuration = duration } - - // Tingwu: TaskKey, StatusText + + // Tingwu: TaskKey if let taskKey = data["TaskKey"] as? String { $0.taskKey = taskKey } else { // Fallback for Volcengine: Use the Request ID (taskId) as Task Key $0.taskKey = taskId } - - if let statusText = data["StatusText"] as? String { - $0.statusText = statusText - } } } @@ -230,15 +267,13 @@ class PollingNode: PipelineNode { // Fetch complete data for database storage let overviewData = await fetchOverviewData(from: result, service: services.transcriptionService) let transcriptData = await fetchTranscriptData(from: result, service: services.transcriptionService) - let conversationData = await fetchConversationData(from: result, service: services.transcriptionService) let rawData = await fetchRawData(from: data, service: services.transcriptionService) - + // 4. Write Output board.updateChannel(channelId) { $0.transcript = TingwuResult(text: transcriptText, summary: summaryText) $0.overviewData = overviewData $0.transcriptData = transcriptData - $0.conversationData = conversationData $0.rawData = rawData } } else if let result = data?["result"] as? [String: Any] { @@ -246,15 +281,14 @@ class PollingNode: PipelineNode { // Direct parsing from the result object // result contains 'text' and 'utterances' let transcriptText = TranscriptParser.buildTranscriptText(from: result) - + let rawData = await fetchRawData(from: data, service: services.transcriptionService) let transcriptData = await fetchRawData(from: result, service: services.transcriptionService) // Save result as transcript data - + board.updateChannel(channelId) { $0.transcript = TingwuResult(text: transcriptText, summary: nil) $0.overviewData = nil // No summary yet $0.transcriptData = transcriptData - $0.conversationData = nil $0.rawData = rawData } } @@ -336,17 +370,7 @@ class PollingNode: PipelineNode { } return nil } - - private func fetchConversationData(from result: [String: Any], service: TranscriptionService) async -> String? { - // Try to fetch conversation data if available - if let conversationUrl = result["Conversation"] as? String { - if let data = try? await service.fetchJSON(url: conversationUrl) { - return jsonString(from: data) - } - } - return nil - } - + private func fetchRawData(from data: [String: Any]?, service: TranscriptionService) async -> String? { // Store the complete raw response data guard let data = data else { return nil } diff --git a/Sources/VoiceMemo/Services/Storage/MySQLStorage.swift b/Sources/VoiceMemo/Services/Storage/MySQLStorage.swift index fd817fe..caf95ea 100644 --- a/Sources/VoiceMemo/Services/Storage/MySQLStorage.swift +++ b/Sources/VoiceMemo/Services/Storage/MySQLStorage.swift @@ -79,19 +79,17 @@ final class MySQLStorage: StorageProvider, @unchecked Sendable { created_at DATETIME NOT NULL, recording_id VARCHAR(255) NOT NULL, local_file_path TEXT NOT NULL, + raw_local_file_path TEXT, oss_url TEXT, - tingwu_task_id VARCHAR(255), + transcription_task_id VARCHAR(255), status VARCHAR(50) NOT NULL, title TEXT NOT NULL, - raw_response TEXT, transcript LONGTEXT, summary TEXT, key_points TEXT, action_items TEXT, last_error TEXT, task_key VARCHAR(255), - api_status VARCHAR(50), - status_text TEXT, biz_duration INT, output_mp3_path TEXT, last_successful_status VARCHAR(50), @@ -100,7 +98,6 @@ final class MySQLStorage: StorageProvider, @unchecked Sendable { original_oss_url TEXT, overview_data LONGTEXT, transcript_data LONGTEXT, - conversation_data LONGTEXT, raw_data LONGTEXT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; """ @@ -125,58 +122,53 @@ final class MySQLStorage: StorageProvider, @unchecked Sendable { }.get() } + if !existingColumns.contains("raw_local_file_path") { + _ = try await pool.withConnection { conn in + conn.query("ALTER TABLE meeting_tasks ADD COLUMN raw_local_file_path TEXT") + }.get() + } + // Migration for Complete Poll Results if !existingColumns.contains("overview_data") { log("MySQLStorage createTableIfNeeded: Adding overview_data column") _ = try await pool.withConnection { conn in - conn.query("ALTER TABLE meeting_tasks ADD COLUMN overview_data TEXT") + conn.query("ALTER TABLE meeting_tasks ADD COLUMN overview_data LONGTEXT") }.get() } - + if !existingColumns.contains("transcript_data") { log("MySQLStorage createTableIfNeeded: Adding transcript_data column") _ = try await pool.withConnection { conn in - conn.query("ALTER TABLE meeting_tasks ADD COLUMN transcript_data TEXT") + conn.query("ALTER TABLE meeting_tasks ADD COLUMN transcript_data LONGTEXT") }.get() } - - if !existingColumns.contains("conversation_data") { - log("MySQLStorage createTableIfNeeded: Adding conversation_data column") - _ = try await pool.withConnection { conn in - conn.query("ALTER TABLE meeting_tasks ADD COLUMN conversation_data TEXT") - }.get() - } - + if !existingColumns.contains("raw_data") { log("MySQLStorage createTableIfNeeded: Adding raw_data column") _ = try await pool.withConnection { conn in - conn.query("ALTER TABLE meeting_tasks ADD COLUMN raw_data TEXT") + conn.query("ALTER TABLE meeting_tasks ADD COLUMN raw_data LONGTEXT") }.get() } - + log("MySQLStorage createTableIfNeeded: Table creation/migration completed") - + // Upgrade existing text columns to LONGTEXT to support large data _ = try? await pool.withConnection { conn in conn.query("ALTER TABLE meeting_tasks MODIFY COLUMN transcript LONGTEXT") }.get() - + _ = try? await pool.withConnection { conn in conn.query("ALTER TABLE meeting_tasks MODIFY COLUMN overview_data LONGTEXT") }.get() - + _ = try? await pool.withConnection { conn in conn.query("ALTER TABLE meeting_tasks MODIFY COLUMN transcript_data LONGTEXT") }.get() - - _ = try? await pool.withConnection { conn in - conn.query("ALTER TABLE meeting_tasks MODIFY COLUMN conversation_data LONGTEXT") - }.get() - + _ = try? await pool.withConnection { conn in conn.query("ALTER TABLE meeting_tasks MODIFY COLUMN raw_data LONGTEXT") }.get() - + log("MySQLStorage createTableIfNeeded: Column upgrade completed") } @@ -199,53 +191,50 @@ final class MySQLStorage: StorageProvider, @unchecked Sendable { log("MySQLStorage saveTask: Saving task \(task.id)") log("MySQLStorage saveTask: overviewData = \(task.overviewData?.prefix(100) ?? "nil")") log("MySQLStorage saveTask: transcriptData = \(task.transcriptData?.prefix(100) ?? "nil")") - log("MySQLStorage saveTask: conversationData = \(task.conversationData?.prefix(100) ?? "nil")") log("MySQLStorage saveTask: rawData = \(task.rawData?.prefix(100) ?? "nil")") - + let sql = """ INSERT INTO meeting_tasks ( - id, created_at, recording_id, local_file_path, oss_url, tingwu_task_id, - status, title, raw_response, transcript, summary, key_points, - action_items, last_error, task_key, api_status, status_text, + id, created_at, recording_id, local_file_path, raw_local_file_path, oss_url, transcription_task_id, + status, title, transcript, summary, key_points, + action_items, last_error, task_key, biz_duration, output_mp3_path, last_successful_status, failed_step, retry_count, original_oss_url, - overview_data, transcript_data, conversation_data, raw_data + overview_data, transcript_data, raw_data ) VALUES ( - ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ON DUPLICATE KEY UPDATE - recording_id=VALUES(recording_id), local_file_path=VALUES(local_file_path), - oss_url=VALUES(oss_url), tingwu_task_id=VALUES(tingwu_task_id), - status=VALUES(status), title=VALUES(title), raw_response=VALUES(raw_response), + recording_id=VALUES(recording_id), local_file_path=VALUES(local_file_path), raw_local_file_path=VALUES(raw_local_file_path), + oss_url=VALUES(oss_url), transcription_task_id=VALUES(transcription_task_id), + status=VALUES(status), title=VALUES(title), transcript=VALUES(transcript), summary=VALUES(summary), key_points=VALUES(key_points), action_items=VALUES(action_items), last_error=VALUES(last_error), - task_key=VALUES(task_key), api_status=VALUES(api_status), status_text=VALUES(status_text), + task_key=VALUES(task_key), biz_duration=VALUES(biz_duration), output_mp3_path=VALUES(output_mp3_path), last_successful_status=VALUES(last_successful_status), failed_step=VALUES(failed_step), retry_count=VALUES(retry_count), original_oss_url=VALUES(original_oss_url), overview_data=VALUES(overview_data), transcript_data=VALUES(transcript_data), - conversation_data=VALUES(conversation_data), raw_data=VALUES(raw_data); + raw_data=VALUES(raw_data); """ - + _ = try await pool.withConnection { conn in let binds: [MySQLData] = [ MySQLData(string: task.id.uuidString), MySQLData(date: task.createdAt), MySQLData(string: task.recordingId), MySQLData(string: task.localFilePath), + task.rawLocalFilePath.map { MySQLData(string: $0) } ?? .null, task.ossUrl.map { MySQLData(string: $0) } ?? .null, - task.tingwuTaskId.map { MySQLData(string: $0) } ?? .null, + task.transcriptionTaskId.map { MySQLData(string: $0) } ?? .null, MySQLData(string: task.status.rawValue), MySQLData(string: task.title), - task.rawResponse.map { MySQLData(string: $0) } ?? .null, task.transcript.map { MySQLData(string: $0) } ?? .null, task.summary.map { MySQLData(string: $0) } ?? .null, task.keyPoints.map { MySQLData(string: $0) } ?? .null, task.actionItems.map { MySQLData(string: $0) } ?? .null, task.lastError.map { MySQLData(string: $0) } ?? .null, task.taskKey.map { MySQLData(string: $0) } ?? .null, - task.apiStatus.map { MySQLData(string: $0) } ?? .null, - task.statusText.map { MySQLData(string: $0) } ?? .null, task.bizDuration.map { MySQLData(int: $0) } ?? .null, task.outputMp3Path.map { MySQLData(string: $0) } ?? .null, task.lastSuccessfulStatus.map { MySQLData(string: $0.rawValue) } ?? .null, @@ -254,7 +243,6 @@ final class MySQLStorage: StorageProvider, @unchecked Sendable { task.originalOssUrl.map { MySQLData(string: $0) } ?? .null, task.overviewData.map { MySQLData(string: $0) } ?? .null, task.transcriptData.map { MySQLData(string: $0) } ?? .null, - task.conversationData.map { MySQLData(string: $0) } ?? .null, task.rawData.map { MySQLData(string: $0) } ?? .null ] return conn.query(sql, binds) @@ -310,26 +298,24 @@ final class MySQLStorage: StorageProvider, @unchecked Sendable { let status = MeetingTaskStatus.from(rawValue: statusRaw) else { return nil } - + let task = MeetingTask(recordingId: recordingId, localFilePath: localFilePath, title: title) task.id = uuid task.createdAt = createdAt task.status = status - + + task.rawLocalFilePath = row.column("raw_local_file_path")?.string task.ossUrl = row.column("oss_url")?.string - task.tingwuTaskId = row.column("tingwu_task_id")?.string - task.rawResponse = row.column("raw_response")?.string + task.transcriptionTaskId = row.column("transcription_task_id")?.string task.transcript = row.column("transcript")?.string task.summary = row.column("summary")?.string task.keyPoints = row.column("key_points")?.string task.actionItems = row.column("action_items")?.string task.lastError = row.column("last_error")?.string task.taskKey = row.column("task_key")?.string - task.apiStatus = row.column("api_status")?.string - task.statusText = row.column("status_text")?.string task.bizDuration = row.column("biz_duration")?.int task.outputMp3Path = row.column("output_mp3_path")?.string - + if let successStatusRaw = row.column("last_successful_status")?.string, let successStatus = MeetingTaskStatus.from(rawValue: successStatusRaw) { task.lastSuccessfulStatus = successStatus @@ -339,13 +325,12 @@ final class MySQLStorage: StorageProvider, @unchecked Sendable { task.failedStep = failedStep } task.retryCount = row.column("retry_count")?.int ?? 0 - + task.originalOssUrl = row.column("original_oss_url")?.string task.overviewData = row.column("overview_data")?.string task.transcriptData = row.column("transcript_data")?.string - task.conversationData = row.column("conversation_data")?.string task.rawData = row.column("raw_data")?.string - + return task } } diff --git a/Sources/VoiceMemo/Services/Storage/SQLiteStorage.swift b/Sources/VoiceMemo/Services/Storage/SQLiteStorage.swift index 6175753..a2e1441 100644 --- a/Sources/VoiceMemo/Services/Storage/SQLiteStorage.swift +++ b/Sources/VoiceMemo/Services/Storage/SQLiteStorage.swift @@ -10,37 +10,33 @@ class SQLiteStorage: StorageProvider { private let createdAt = Expression("created_at") private let recordingId = Expression("recording_id") private let localFilePath = Expression("local_file_path") + private let rawLocalFilePath = Expression("raw_local_file_path") private let ossUrl = Expression("oss_url") - private let tingwuTaskId = Expression("tingwu_task_id") + private let transcriptionTaskId = Expression("transcription_task_id") private let status = Expression("status") private let title = Expression("title") - private let rawResponse = Expression("raw_response") private let transcript = Expression("transcript") private let summary = Expression("summary") private let keyPoints = Expression("key_points") private let actionItems = Expression("action_items") private let lastError = Expression("last_error") - - // New Fields + + // Task Info Fields private let taskKey = Expression("task_key") - private let apiStatus = Expression("api_status") - private let statusText = Expression("status_text") private let bizDuration = Expression("biz_duration") private let outputMp3Path = Expression("output_mp3_path") - - // New Fields for Retry + + // Retry Fields private let lastSuccessfulStatus = Expression("last_successful_status") private let failedStep = Expression("failed_step") private let retryCount = Expression("retry_count") - - // New Fields for Separated Mode (Removed) - + + // OSS URL Fields private let originalOssUrl = Expression("original_oss_url") - - // New Fields for Complete Poll Results + + // Poll Results Storage private let overviewData = Expression("overview_data") - private let transcriptData = Expression("transcript_data") - private let conversationData = Expression("conversation_data") + private let transcriptData = Expression("transcript_data") private let rawData = Expression("raw_data") init() { @@ -94,52 +90,41 @@ class SQLiteStorage: StorageProvider { t.column(createdAt) t.column(recordingId) t.column(localFilePath) + t.column(rawLocalFilePath) t.column(ossUrl) - t.column(tingwuTaskId) + t.column(transcriptionTaskId) t.column(status) t.column(title) - t.column(rawResponse) t.column(transcript) t.column(summary) t.column(keyPoints) t.column(actionItems) t.column(lastError) t.column(taskKey) - t.column(apiStatus) - t.column(statusText) t.column(bizDuration) t.column(outputMp3Path) t.column(lastSuccessfulStatus) t.column(failedStep) t.column(retryCount, defaultValue: 0) - - // Separated Mode (Removed) t.column(originalOssUrl) t.column(overviewData) t.column(transcriptData) - t.column(conversationData) t.column(rawData) }) - + // Migration for existing tables - only add if they don't exist let existingColumns = getColumnNames() - + if !existingColumns.contains("task_key") { _ = try? db.run(tasks.addColumn(taskKey)) } - if !existingColumns.contains("api_status") { _ = try? db.run(tasks.addColumn(apiStatus)) } - if !existingColumns.contains("status_text") { _ = try? db.run(tasks.addColumn(statusText)) } if !existingColumns.contains("biz_duration") { _ = try? db.run(tasks.addColumn(bizDuration)) } if !existingColumns.contains("output_mp3_path") { _ = try? db.run(tasks.addColumn(outputMp3Path)) } + if !existingColumns.contains("raw_local_file_path") { _ = try? db.run(tasks.addColumn(rawLocalFilePath)) } if !existingColumns.contains("last_successful_status") { _ = try? db.run(tasks.addColumn(lastSuccessfulStatus)) } if !existingColumns.contains("failed_step") { _ = try? db.run(tasks.addColumn(failedStep)) } if !existingColumns.contains("retry_count") { _ = try? db.run(tasks.addColumn(retryCount, defaultValue: 0)) } - - // Migration for Separated Mode (Removed) if !existingColumns.contains("original_oss_url") { _ = try? db.run(tasks.addColumn(originalOssUrl)) } - - // Migration for Complete Poll Results if !existingColumns.contains("overview_data") { _ = try? db.run(tasks.addColumn(overviewData)) } if !existingColumns.contains("transcript_data") { _ = try? db.run(tasks.addColumn(transcriptData)) } - if !existingColumns.contains("conversation_data") { _ = try? db.run(tasks.addColumn(conversationData)) } if !existingColumns.contains("raw_data") { _ = try? db.run(tasks.addColumn(rawData)) } } catch { print("Create table error: \(error)") @@ -165,25 +150,23 @@ class SQLiteStorage: StorageProvider { func saveTask(_ task: MeetingTask) async throws { guard let db = db else { return } - + let insert = tasks.insert(or: .replace, id <- task.id.uuidString, createdAt <- task.createdAt, recordingId <- task.recordingId, localFilePath <- task.localFilePath, + rawLocalFilePath <- task.rawLocalFilePath, ossUrl <- task.ossUrl, - tingwuTaskId <- task.tingwuTaskId, + transcriptionTaskId <- task.transcriptionTaskId, status <- task.status.rawValue, title <- task.title, - rawResponse <- task.rawResponse, transcript <- task.transcript, summary <- task.summary, keyPoints <- task.keyPoints, actionItems <- task.actionItems, lastError <- task.lastError, taskKey <- task.taskKey, - apiStatus <- task.apiStatus, - statusText <- task.statusText, bizDuration <- task.bizDuration, outputMp3Path <- task.outputMp3Path, lastSuccessfulStatus <- task.lastSuccessfulStatus?.rawValue, @@ -192,7 +175,6 @@ class SQLiteStorage: StorageProvider { originalOssUrl <- task.originalOssUrl, overviewData <- task.overviewData, transcriptData <- task.transcriptData, - conversationData <- task.conversationData, rawData <- task.rawData ) try db.run(insert) @@ -200,37 +182,35 @@ class SQLiteStorage: StorageProvider { func fetchTasks() async throws -> [MeetingTask] { guard let db = db else { return [] } - + var results: [MeetingTask] = [] - + for row in try db.prepare(tasks.order(createdAt.desc)) { let task = MeetingTask( recordingId: row[recordingId], localFilePath: row[localFilePath], title: row[title] ) - + if let uuid = UUID(uuidString: row[id]) { task.id = uuid } task.createdAt = row[createdAt] + task.rawLocalFilePath = row[rawLocalFilePath] task.ossUrl = row[ossUrl] - task.tingwuTaskId = row[tingwuTaskId] + task.transcriptionTaskId = row[transcriptionTaskId] if let statusEnum = MeetingTaskStatus.from(rawValue: row[status]) { task.status = statusEnum } - task.rawResponse = row[rawResponse] task.transcript = row[transcript] task.summary = row[summary] task.keyPoints = row[keyPoints] task.actionItems = row[actionItems] task.lastError = row[lastError] task.taskKey = row[taskKey] - task.apiStatus = row[apiStatus] - task.statusText = row[statusText] task.bizDuration = row[bizDuration] task.outputMp3Path = row[outputMp3Path] - + if let successStatusRaw = row[lastSuccessfulStatus], let successStatus = MeetingTaskStatus.from(rawValue: successStatusRaw) { task.lastSuccessfulStatus = successStatus } @@ -238,16 +218,15 @@ class SQLiteStorage: StorageProvider { task.failedStep = failedStepEnum } task.retryCount = row[retryCount] - + task.originalOssUrl = row[originalOssUrl] task.overviewData = row[overviewData] task.transcriptData = row[transcriptData] - task.conversationData = row[conversationData] task.rawData = row[rawData] - + results.append(task) } - + return results } @@ -266,38 +245,34 @@ class SQLiteStorage: StorageProvider { func getTask(id: UUID) async throws -> MeetingTask? { guard let db = db else { return nil } let query = tasks.filter(self.id == id.uuidString) - + guard let row = try db.pluck(query) else { return nil } - + let task = MeetingTask( recordingId: row[recordingId], localFilePath: row[localFilePath], title: row[title] ) - + if let uuid = UUID(uuidString: row[self.id]) { task.id = uuid } task.createdAt = row[createdAt] + task.rawLocalFilePath = row[rawLocalFilePath] task.ossUrl = row[ossUrl] - task.tingwuTaskId = row[tingwuTaskId] + task.transcriptionTaskId = row[transcriptionTaskId] if let statusEnum = MeetingTaskStatus.from(rawValue: row[status]) { task.status = statusEnum } - // ... (rest of mapping) ... - // To avoid code duplication, I should refactor the mapping logic, but for now I'll just copy it. - task.rawResponse = row[rawResponse] task.transcript = row[transcript] task.summary = row[summary] task.keyPoints = row[keyPoints] task.actionItems = row[actionItems] task.lastError = row[lastError] task.taskKey = row[taskKey] - task.apiStatus = row[apiStatus] - task.statusText = row[statusText] task.bizDuration = row[bizDuration] task.outputMp3Path = row[outputMp3Path] - + if let successStatusRaw = row[lastSuccessfulStatus], let successStatus = MeetingTaskStatus.from(rawValue: successStatusRaw) { task.lastSuccessfulStatus = successStatus } @@ -305,13 +280,12 @@ class SQLiteStorage: StorageProvider { task.failedStep = failedStepEnum } task.retryCount = row[retryCount] - + task.originalOssUrl = row[originalOssUrl] task.overviewData = row[overviewData] task.transcriptData = row[transcriptData] - task.conversationData = row[conversationData] task.rawData = row[rawData] - + return task } } diff --git a/Sources/VoiceMemo/Views/ResultView.swift b/Sources/VoiceMemo/Views/ResultView.swift index 0b8507a..96c0521 100644 --- a/Sources/VoiceMemo/Views/ResultView.swift +++ b/Sources/VoiceMemo/Views/ResultView.swift @@ -126,7 +126,7 @@ struct ResultView: View { case .transcript: TranscriptView(text: task.derivedTranscriptText() ?? "No transcript available.") case .raw: - RawDataView(text: task.rawData ?? task.rawResponse ?? "No raw response.") + RawDataView(text: task.rawData ?? "No raw data available.") case .pipeline: PipelineView(task: task, settings: settings, playback: playback) { withAnimation { @@ -150,7 +150,18 @@ struct ResultView: View { // Prepare summary attachment if settings.emailAttachSummary { - let mdContent = task.markdownSummary() + var mdContent = task.markdownSummary() + if settings.emailAttachAudio { + var links: [String] = [] + if let url = task.ossUrl { links.append("- Processed: \(url)") } + if let url = task.originalOssUrl { links.append("- Original: \(url)") } + if let url = task.outputMp3Path { links.append("- MP3: \(url)") } + if !links.isEmpty { + mdContent += "## Audio Links\n" + mdContent += links.joined(separator: "\n") + mdContent += "\n\n" + } + } let mdFilename = baseFilename.appending(".md") let mdUrl = FileManager.default.temporaryDirectory.appendingPathComponent(mdFilename) if let mdData = mdContent.data(using: .utf8) { @@ -160,25 +171,6 @@ struct ResultView: View { } } - // Prepare audio attachment - if settings.emailAttachAudio { - // Check for local file first - var audioPath: String? - if FileManager.default.fileExists(atPath: task.localFilePath) { - audioPath = task.localFilePath - } else if let mp3UrlString = task.outputMp3Path, let mp3Url = URL(string: mp3UrlString) { - // Try to download from URL - if let tempMp3Url = try? await downloadFile(from: mp3Url) { - tempFiles.append(tempMp3Url) - audioPath = tempMp3Url.path - } - } - - if let path = audioPath { - attachmentPaths.append(path) - } - } - // Prepare transcript attachment if settings.emailAttachTranscript, let transcriptText = task.derivedTranscriptText() { let transcriptFilename = baseFilename.appending("-transcript.txt") @@ -192,7 +184,7 @@ struct ResultView: View { // Prepare raw data attachment if settings.emailAttachRawData { - if let rawDataStr = task.rawData ?? task.rawResponse { + if let rawDataStr = task.rawData { let rawFilename = baseFilename.appending("-raw.json") let rawUrl = FileManager.default.temporaryDirectory.appendingPathComponent(rawFilename) if let rawData = rawDataStr.data(using: .utf8) { @@ -228,13 +220,6 @@ struct ResultView: View { emailStatus = nil } - private func downloadFile(from url: URL) async throws -> URL { - let (tempUrl, _) = try await URLSession.shared.download(from: url) - let destination = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent) - try FileManager.default.moveItem(at: tempUrl, to: destination) - return destination - } - private func exportMarkdown() { let panel = NSSavePanel() panel.allowedContentTypes = [UTType(filenameExtension: "md") ?? .plainText] @@ -307,35 +292,7 @@ struct TaskInfoView: View { } } } - - if let status = task.apiStatus { - GridRow { - Text("Status") - .foregroundColor(.secondary) - .gridColumnAlignment(.trailing) - - HStack { - Circle() - .fill(statusColor(status)) - .frame(width: 8, height: 8) - Text(status) - .font(.subheadline) - } - } - } - - if let error = task.statusText, !error.isEmpty { - GridRow { - Text("Message") - .foregroundColor(.secondary) - .gridColumnAlignment(.trailing) - - Text(error) - .font(.subheadline) - .foregroundColor(.red) - } - } - + if let duration = task.bizDuration { GridRow { Text("Duration") @@ -370,18 +327,6 @@ struct TaskInfoView: View { .stroke(Color.gray.opacity(0.1), lineWidth: 1) ) } - - private func statusColor(_ status: String) -> Color { - let s = status.uppercased() - if s == "SUCCESS" || s == "COMPLETED" || s == "20000000" { - return .green - } else if s == "RUNNING" || s == "POLLING" { - return .blue - } else if s == "FAILED" { - return .red - } - return .secondary - } } struct OverviewView: View { diff --git a/Sources/VoiceMemo/Views/SettingsView.swift b/Sources/VoiceMemo/Views/SettingsView.swift index 0290d32..dd593dc 100644 --- a/Sources/VoiceMemo/Views/SettingsView.swift +++ b/Sources/VoiceMemo/Views/SettingsView.swift @@ -704,11 +704,11 @@ struct SettingsView: View { .foregroundColor(.secondary) } - FormRow(label: "Audio") { + FormRow(label: "Audio Link") { Toggle("", isOn: $settings.emailAttachAudio) .toggleStyle(.switch) .labelsHidden() - Text("Include audio recording file") + Text("Include audio link in summary markdown") .font(.caption) .foregroundColor(.secondary) } diff --git a/Tests/VoiceMemoTests/PipelineBoardTests.swift b/Tests/VoiceMemoTests/PipelineBoardTests.swift index e3e73bc..7622e8f 100644 --- a/Tests/VoiceMemoTests/PipelineBoardTests.swift +++ b/Tests/VoiceMemoTests/PipelineBoardTests.swift @@ -77,8 +77,8 @@ final class PipelineBoardTests: XCTestCase { channel.rawAudioOssURL = "https://oss.example.com/test.m4a" channel.processedAudioPath = "/tmp/test_48k.m4a" channel.processedAudioOssURL = "https://oss.example.com/test_48k.m4a" - channel.tingwuTaskId = "task-123" - channel.tingwuTaskStatus = "RUNNING" + channel.transcriptionTaskId = "task-123" + channel.transcriptionTaskStatus = "RUNNING" } let channel = board.channels[0] @@ -87,8 +87,8 @@ final class PipelineBoardTests: XCTestCase { XCTAssertEqual(channel?.rawAudioOssURL, "https://oss.example.com/test.m4a") XCTAssertEqual(channel?.processedAudioPath, "/tmp/test_48k.m4a") XCTAssertEqual(channel?.processedAudioOssURL, "https://oss.example.com/test_48k.m4a") - XCTAssertEqual(channel?.tingwuTaskId, "task-123") - XCTAssertEqual(channel?.tingwuTaskStatus, "RUNNING") + XCTAssertEqual(channel?.transcriptionTaskId, "task-123") + XCTAssertEqual(channel?.transcriptionTaskStatus, "RUNNING") } func testFormattedDatePath() { diff --git a/doc/03-pipeline-asr-oss.md b/doc/03-pipeline-asr-oss.md index 4fe46eb..eeb6b5d 100644 --- a/doc/03-pipeline-asr-oss.md +++ b/doc/03-pipeline-asr-oss.md @@ -67,7 +67,7 @@ Provider selection is controlled by `SettingsStore.asrProvider` and wired in `Me - Purpose: Backup the original high-fidelity audio (e.g., m4a/wav) before transcoding. - Object key format: - - `"//original."` + - `"//_raw.m4a"` - Updates: - `task.originalFileUrl` - `task.status`: `recorded` → `uploadingRaw` → `uploadedRaw` @@ -77,7 +77,10 @@ Provider selection is controlled by `SettingsStore.asrProvider` and wired in `Me `MeetingPipelineManager.transcode()` triggers the full pipeline start. The actual work is performed by `TranscodeNode`. - Input: `task.localFilePath` (typically `...mixed.m4a`) -- Output: `mixed_48k.m4a` in the same folder +- Output: `recording--_mixed_48k.m4a` (Input filename + timestamp + `_48k`) +- Logic: + - Appends timestamp (yyyyMMdd-HHmmss) to ensure uniqueness + - Force overwrites if file exists - Uses `AVAssetExportSession` with preset `AVAssetExportPresetAppleM4A` - Updates: - `task.localFilePath` to the transcoded file @@ -88,9 +91,9 @@ Provider selection is controlled by `SettingsStore.asrProvider` and wired in `Me `UploadNode` → `OSSService.uploadFile()`: - Object key format: - - `"//mixed.m4a"` + - `"//.m4a"` - Notes: - - Local transcoded filename uses `mixed_48k.m4a`, but the OSS object key remains `mixed.m4a`. + - The OSS object key will directly use the local transcoded filename (e.g., `recording-...-mixed_48k.m4a`) to ensure uniqueness and traceability. - Returns: - `publicUrl` computed as `https://./` - Updates: @@ -122,7 +125,7 @@ Feature toggles influence parameters: On success: -- Saves `task.tingwuTaskId` +- Saves `task.transcriptionTaskId` - Moves status to `polling` ### ByteDance Volcengine @@ -156,7 +159,7 @@ On success: - Calls `activeTranscriptionService.getTaskInfo(taskId:)` - On `SUCCESS` / `COMPLETED`: - - Persists raw `Data` object (pretty JSON) into `task.rawResponse` + - Persists raw `Data` object (pretty JSON) into `task.rawData` - Extracts: - Uses **`TranscriptParser`** to unify transcript result parsing across providers. - Transcript: Provider-specific format → `TranscriptParser` parses to text. diff --git a/doc/03-pipeline-asr-oss.zh-CN.md b/doc/03-pipeline-asr-oss.zh-CN.md index fd76511..4deb00f 100644 --- a/doc/03-pipeline-asr-oss.zh-CN.md +++ b/doc/03-pipeline-asr-oss.zh-CN.md @@ -77,7 +77,7 @@ `MeetingPipelineManager.transcode()` 用于触发流水线开始,实际转码由 `TranscodeNode` 执行: - 输入:`task.localFilePath`(通常是 `...mixed.m4a`) -- 输出:同目录下的 `mixed_48k.m4a` +- 输出:同目录下的 `mixed_48k.m4a`(或 `recording-...-mixed_48k.m4a`) - 使用 `AVAssetExportSession` + `AVAssetExportPresetAppleM4A` - 更新: - `task.localFilePath` 指向转码后的文件 @@ -88,9 +88,9 @@ `UploadNode` → `OSSService.uploadFile()`: - ObjectKey 规则: - - `"//mixed.m4a"` + - `"//.m4a"` - 说明: - - 本地转码文件名使用 `mixed_48k.m4a`,但 OSS objectKey 仍保持 `mixed.m4a`。 + - OSS objectKey 将直接使用本地转码后的文件名(例如 `recording-...-mixed_48k.m4a`),确保唯一性和可追溯性。 - 返回: - `publicUrl = https://./` - 更新: @@ -114,7 +114,7 @@ - 角色分离:`Transcription.DiarizationEnabled` 和 `SpeakerCount` 成功后: -- 保存 `task.tingwuTaskId` +- 保存 `task.transcriptionTaskId` - 状态进入 `polling` ### 字节跳动火山引擎 (`VolcengineService`) @@ -158,7 +158,7 @@ - **保存**: - `task.transcript`:纯文本格式的对话记录。 - - `task.rawResponse`:原始 JSON 响应备份。 + - `task.rawData`:原始 JSON 响应。 - `task.status`:`completed` 或 `failed`。 ## 转录解析器