Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/VoiceMemo/AudioRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
25 changes: 7 additions & 18 deletions Sources/VoiceMemo/Models/MeetingTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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) {
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -198,22 +193,16 @@ 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] {
if let text = TranscriptParser.buildTranscriptText(from: json) {
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 {
Expand Down
1 change: 1 addition & 0 deletions Sources/VoiceMemo/Services/HistoryStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
87 changes: 53 additions & 34 deletions Sources/VoiceMemo/Services/MeetingPipelineManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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")
Expand All @@ -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) {
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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 }
}

Expand Down
9 changes: 3 additions & 6 deletions Sources/VoiceMemo/Services/Pipeline/PipelineBoard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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. 最终产物
Expand All @@ -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. 错误追踪 (用于单路重试)
Expand Down
Loading