     1→import Foundation
     2→import SwiftData
     3→
     4→/// Utility para importar transcrições externas ao banco de dados
     5→/// Útil para transcrições feitas offline com Whisper
     6→struct TranscriptImporter {
     7→
     8→    /// Importa uma transcrição para o banco de dados
     9→    static func importTranscript(
    10→        youtubeURL: String,
    11→        videoID: String,
    12→        title: String,
    13→        channelName: String,
    14→        transcript: String,
    15→        language: String = "en",
    16→        duration: String? = nil,
    17→        thumbnailURL: String? = nil,
    18→        transcriptSource: String = "whisper",
    19→        modelContext: ModelContext
    20→    ) throws {
    21→
    22→        // Verifica se já existe
    23→        let descriptor = FetchDescriptor<SavedVideo>(
    24→            predicate: #Predicate { $0.videoID == videoID }
    25→        )
    26→
    27→        let existing = try? modelContext.fetch(descriptor)
    28→
    29→        if let existingVideo = existing?.first {
    30→            print("⚠ Vídeo já existe no banco de dados: \(existingVideo.title)")
    31→            print("ID: \(existingVideo.id)")
    32→            return
    33→        }
    34→
    35→        // Cria novo SavedVideo
    36→        let savedVideo = SavedVideo(
    37→            youtubeURL: youtubeURL,
    38→            videoID: videoID,
    39→            title: title,
    40→            channelName: channelName,
    41→            transcript: transcript,
    42→            language: language,
    43→            duration: duration,
    44→            thumbnailURL: thumbnailURL,
    45→            isFavorite: false
    46→        )
    47→
    48→        // Define a origem da transcrição
    49→        savedVideo.transcriptSource = transcriptSource
    50→
    51→        // Salva no banco de dados
    52→        modelContext.insert(savedVideo)
    53→        try modelContext.save()
    54→
    55→        print("✅ Transcrição importada com sucesso!")
    56→        print("📺 Título: \(title)")
    57→        print("🆔 Video ID: \(videoID)")
    58→        print("📝 Palavras: \(transcript.split(separator: " ").count)")
    59→        print("🤖 Origem: \(transcriptSource)")
    60→    }
    61→}
    62→
    63→// MARK: - Import from File
    64→
    65→extension TranscriptImporter {
    66→
    67→    /// Importa transcrição do arquivo temporário do Palantir
    68→    static func importPalantirTranscript(modelContext: ModelContext) throws {
    69→        let transcriptPath = "/tmp/palantir_transcript.txt"
    70→
    71→        guard let transcript = try? String(contentsOfFile: transcriptPath, encoding: .utf8) else {
    72→            throw ImportError.fileNotFound(transcriptPath)
    73→        }
    74→
    75→        try importTranscript(
    76→            youtubeURL: "https://www.youtube.com/watch?v=UjkRz9HkldU",
    77→            videoID: "UjkRz9HkldU",
    78→            title: "Paragon 2025",
    79→            channelName: "Palantir",
    80→            transcript: transcript,
    81→            language: "en",
    82→            duration: "53 minutes",
    83→            thumbnailURL: nil,
    84→            transcriptSource: "whisper",
    85→            modelContext: modelContext
    86→        )
    87→    }
    88→}
    89→
    90→// MARK: - Errors
    91→
    92→enum ImportError: LocalizedError {
    93→    case fileNotFound(String)
    94→    case duplicateVideo(String)
    95→
    96→    var errorDescription: String? {
    97→        switch self {
    98→        case .fileNotFound(let path):
    99→            return "Arquivo não encontrado: \(path)"
   100→        case .duplicateVideo(let videoID):

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation
import SwiftData

/// Utility para importar transcrições externas ao banco de dados
/// Útil para transcrições feitas offline com Whisper
struct TranscriptImporter {

    /// Importa uma transcrição para o banco de dados
    static func importTranscript(
        youtubeURL: String,
        videoID: String,
        title: String,
        channelName: String,
        transcript: String,
        language: String = "en",
        duration: String? = nil,
        thumbnailURL: String? = nil,
        transcriptSource: String = "whisper",
        modelContext: ModelContext
    ) throws {

        // Verifica se já existe
        let descriptor = FetchDescriptor<SavedVideo>(
            predicate: #Predicate { $0.videoID == videoID }
        )

        let existing = try? modelContext.fetch(descriptor)

        if let existingVideo = existing?.first {
            print("⚠ Vídeo já existe no banco de dados: \(existingVideo.title)")
            print("ID: \(existingVideo.id)")
            return
        }

        // Cria novo SavedVideo
        let savedVideo = SavedVideo(
            youtubeURL: youtubeURL,
            videoID: videoID,
            title: title,
            channelName: channelName,
            transcript: transcript,
            language: language,
            duration: duration,
            thumbnailURL: thumbnailURL,
            isFavorite: false
        )

        // Define a origem da transcrição
        savedVideo.transcriptSource = transcriptSource

        // Salva no banco de dados
        modelContext.insert(savedVideo)
        try modelContext.save()

        print("✅ Transcrição importada com sucesso!")
        print("📺 Título: \(title)")
        print("🆔 Video ID: \(videoID)")
        print("📝 Palavras: \(transcript.split(separator: " ").count)")
        print("🤖 Origem: \(transcriptSource)")
    }
}

// MARK: - Import from File

extension TranscriptImporter {

    /// Importa transcrição do arquivo temporário do Palantir
    static func importPalantirTranscript(modelContext: ModelContext) throws {
        let transcriptPath = "/tmp/palantir_transcript.txt"

        guard let transcript = try? String(contentsOfFile: transcriptPath, encoding: .utf8) else {
            throw ImportError.fileNotFound(transcriptPath)
        }

        try importTranscript(
            youtubeURL: "https://www.youtube.com/watch?v=UjkRz9HkldU",
            videoID: "UjkRz9HkldU",
            title: "Paragon 2025",
            channelName: "Palantir",
            transcript: transcript,
            language: "en",
            duration: "53 minutes",
            thumbnailURL: nil,
            transcriptSource: "whisper",
            modelContext: modelContext
        )
    }
}

// MARK: - Errors

enum ImportError: LocalizedError {
    case fileNotFound(String)
    case duplicateVideo(String)

    var errorDescription: String? {
        switch self {
        case .fileNotFound(let path):
            return "Arquivo não encontrado: \(path)"
        case .duplicateVideo(let videoID):


Now let me check the ContentView and AppConfiguration to understand the overall flow:


     1→import SwiftUI
     2→import SwiftData
     3→
     4→// MARK: - Video Filter
     5→
     6→enum VideoFilter: String, CaseIterable {
     7→    case all = "All"
     8→    case favorites = "Favorites"
     9→    case recent = "Recent"
    10→
    11→    var icon: String {
    12→        switch self {
    13→        case .all: return "list.bullet"
    14→        case .favorites: return "star.fill"
    15→        case .recent: return "clock"
    16→        }
    17→    }
    18→}
    19→
    20→struct ContentView: View {
    21→    @Environment(\.modelContext) private var modelContext
    22→    @Environment(\.openWindow) private var openWindow
    23→    @Query(sort: \SavedVideo.savedAt, order: .reverse) private var savedVideos: [SavedVideo]
    24→
    25→    @State private var selectedVideo: SavedVideo?
    26→    @State private var showingInput = false
    27→    @State private var selectedFilter: VideoFilter = .all
    28→    @State private var showingDeleteAlert = false
    29→    @State private var videoToDelete: SavedVideo?
    30→
    31→    private var filteredVideos: [SavedVideo] {
    32→        switch selectedFilter {
    33→        case .all:
    34→            return savedVideos
    35→        case .favorites:
    36→            return savedVideos.filter { $0.isFavorite }
    37→        case .recent:
    38→            let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date()
    39→            return savedVideos.filter { $0.savedAt > oneWeekAgo }
    40→        }
    41→    }
    42→
    43→    var body: some View {
    44→        NavigationSplitView {
    45→            SidebarView(
    46→                savedVideos: filteredVideos,
    47→                selectedVideo: $selectedVideo,
    48→                showingInput: $showingInput,
    49→                selectedFilter: $selectedFilter,
    50→                onDelete: confirmDelete,
    51→                onToggleFavorite: toggleFavorite
    52→            )
    53→        } detail: {
    54→            if showingInput {
    55→                TranscriptInputView(
    56→                    onSave: { video in
    57→                        selectedVideo = video
    58→                        showingInput = false
    59→                    }
    60→                )
    61→            } else if let video = selectedVideo {
    62→                TranscriptReadingView(video: video)
    63→            } else {
    64→                EmptyStateView(showingInput: $showingInput)
    65→            }
    66→        }
    67→        .frame(minWidth: 900, minHeight: 600)
    68→        .alert("Delete Transcript?", isPresented: $showingDeleteAlert) {
    69→            Button("Cancel", role: .cancel) {
    70→                videoToDelete = nil
    71→            }
    72→            Button("Delete", role: .destructive) {
    73→                if let video = videoToDelete {
    74→                    deleteVideo(video)
    75→                }
    76→                videoToDelete = nil
    77→            }
    78→        } message: {
    79→            if let video = videoToDelete {
    80→                Text("Are you sure you want to delete \"\(video.title)\"? This action cannot be undone.")
    81→            }
    82→        }
    83→        .onReceive(NotificationCenter.default.publisher(for: .newTranscript)) { _ in
    84→            showingInput = true
    85→        }
    86→        .onReceive(NotificationCenter.default.publisher(for: .deleteVideo)) { _ in
    87→            if let video = selectedVideo {
    88→                confirmDelete(video)
    89→            }
    90→        }
    91→        .onReceive(NotificationCenter.default.publisher(for: .openDiagnostics)) { _ in
    92→            openWindow(id: "diagnostics")
    93→        }
    94→        .onReceive(NotificationCenter.default.publisher(for: .importWhisperTranscript)) { _ in
    95→            importWhisperTranscript()
    96→        }
    97→    }
    98→
    99→    private func confirmDelete(_ video: SavedVideo) {
   100→        videoToDelete = video

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import SwiftUI
import SwiftData

// MARK: - Video Filter

enum VideoFilter: String, CaseIterable {
    case all = "All"
    case favorites = "Favorites"
    case recent = "Recent"

    var icon: String {
        switch self {
        case .all: return "list.bullet"
        case .favorites: return "star.fill"
        case .recent: return "clock"
        }
    }
}

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Environment(\.openWindow) private var openWindow
    @Query(sort: \SavedVideo.savedAt, order: .reverse) private var savedVideos: [SavedVideo]

    @State private var selectedVideo: SavedVideo?
    @State private var showingInput = false
    @State private var selectedFilter: VideoFilter = .all
    @State private var showingDeleteAlert = false
    @State private var videoToDelete: SavedVideo?

    private var filteredVideos: [SavedVideo] {
        switch selectedFilter {
        case .all:
            return savedVideos
        case .favorites:
            return savedVideos.filter { $0.isFavorite }
        case .recent:
            let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date()
            return savedVideos.filter { $0.savedAt > oneWeekAgo }
        }
    }

    var body: some View {
        NavigationSplitView {
            SidebarView(
                savedVideos: filteredVideos,
                selectedVideo: $selectedVideo,
                showingInput: $showingInput,
                selectedFilter: $selectedFilter,
                onDelete: confirmDelete,
                onToggleFavorite: toggleFavorite
            )
        } detail: {
            if showingInput {
                TranscriptInputView(
                    onSave: { video in
                        selectedVideo = video
                        showingInput = false
                    }
                )
            } else if let video = selectedVideo {
                TranscriptReadingView(video: video)
            } else {
                EmptyStateView(showingInput: $showingInput)
            }
        }
        .frame(minWidth: 900, minHeight: 600)
        .alert("Delete Transcript?", isPresented: $showingDeleteAlert) {
            Button("Cancel", role: .cancel) {
                videoToDelete = nil
            }
            Button("Delete", role: .destructive) {
                if let video = videoToDelete {
                    deleteVideo(video)
                }
                videoToDelete = nil
            }
        } message: {
            if let video = videoToDelete {
                Text("Are you sure you want to delete \"\(video.title)\"? This action cannot be undone.")
            }
        }
        .onReceive(NotificationCenter.default.publisher(for: .newTranscript)) { _ in
            showingInput = true
        }
        .onReceive(NotificationCenter.default.publisher(for: .deleteVideo)) { _ in
            if let video = selectedVideo {
                confirmDelete(video)
            }
        }
        .onReceive(NotificationCenter.default.publisher(for: .openDiagnostics)) { _ in
            openWindow(id: "diagnostics")
        }
        .onReceive(NotificationCenter.default.publisher(for: .importWhisperTranscript)) { _ in
            importWhisperTranscript()
        }
    }

    private func confirmDelete(_ video: SavedVideo) {
        videoToDelete = video


     1→import Foundation
     2→
     3→// MARK: - App Configuration
     4→
     5→/// Centralized configuration for the app
     6→/// All hard-coded values should be defined here
     7→enum AppConfig {
     8→
     9→    // MARK: - App Info
    10→
    11→    static let appName = "YouTube Transcripts"
    12→    static let bundleIdentifier = "com.neog.YouTubeTranscripts"
    13→    static let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
    14→    static let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1"
    15→
    16→    // MARK: - Network
    17→
    18→    enum Network {
    19→        /// Default LM Studio API URL
    20→        static let defaultLMStudioURL = "http://192.168.1.96:1234/v1"
    21→
    22→        /// Network timeouts
    23→        static let shortTimeout: TimeInterval = 10
    24→        static let defaultTimeout: TimeInterval = 30
    25→        static let longTimeout: TimeInterval = 180
    26→        static let transcriptTimeout: TimeInterval = 600 // 10 minutes for long videos
    27→    }
    28→
    29→    // MARK: - YouTube
    30→
    31→    enum YouTube {
    32→        /// Regex patterns for extracting video ID
    33→        static let videoIDPatterns = [
    34→            #"(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})"#,
    35→            #"^([a-zA-Z0-9_-]{11})$"#
    36→        ]
    37→
    38→        /// Supported languages for transcripts
    39→        static let supportedLanguages: [(code: String, name: String)] = [
    40→            ("en", "English"),
    41→            ("pt", "Portuguese"),
    42→            ("es", "Spanish"),
    43→            ("fr", "French"),
    44→            ("de", "German"),
    45→            ("it", "Italian"),
    46→            ("ja", "Japanese"),
    47→            ("ko", "Korean"),
    48→            ("zh", "Chinese")
    49→        ]
    50→
    51→        /// Default transcript language
    52→        static let defaultLanguage = "en"
    53→    }
    54→
    55→    // MARK: - AI
    56→
    57→    enum AI {
    58→        /// Maximum tokens for transcript processing
    59→        static let maxTranscriptTokens = 6000
    60→
    61→        /// Estimated characters per token
    62→        static let charsPerToken = 4
    63→
    64→        /// Maximum characters for transcript (~24000)
    65→        static var maxTranscriptChars: Int { maxTranscriptTokens * charsPerToken }
    66→
    67→        /// Truncation ratio (60% beginning, 40% end)
    68→        static let beginningRatio = 0.6
    69→
    70→        /// Default max tokens for AI response
    71→        static let defaultMaxResponseTokens = 1000
    72→
    73→        /// Temperature for AI generation
    74→        static let defaultTemperature = 0.7
    75→    }
    76→
    77→    // MARK: - Logging
    78→
    79→    enum Logging {
    80→        /// Buffer size before flushing to database
    81→        static let bufferSize = 50
    82→
    83→        /// Maximum recent logs to keep in memory
    84→        static let maxRecentLogs = 500
    85→
    86→        /// Days to keep logs before cleanup
    87→        static let logRetentionDays = 7
    88→
    89→        /// Analytics buffer size
    90→        static let analyticsBufferSize = 20
    91→    }
    92→
    93→    // MARK: - UI
    94→
    95→    enum UI {
    96→        /// Default font size for reading
    97→        static let defaultFontSize: Double = 16
    98→
    99→        /// Font size range
   100→        static let minFontSize: Double = 12

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation

// MARK: - App Configuration

/// Centralized configuration for the app
/// All hard-coded values should be defined here
enum AppConfig {

    // MARK: - App Info

    static let appName = "YouTube Transcripts"
    static let bundleIdentifier = "com.neog.YouTubeTranscripts"
    static let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
    static let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1"

    // MARK: - Network

    enum Network {
        /// Default LM Studio API URL
        static let defaultLMStudioURL = "http://192.168.1.96:1234/v1"

        /// Network timeouts
        static let shortTimeout: TimeInterval = 10
        static let defaultTimeout: TimeInterval = 30
        static let longTimeout: TimeInterval = 180
        static let transcriptTimeout: TimeInterval = 600 // 10 minutes for long videos
    }

    // MARK: - YouTube

    enum YouTube {
        /// Regex patterns for extracting video ID
        static let videoIDPatterns = [
            #"(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})"#,
            #"^([a-zA-Z0-9_-]{11})$"#
        ]

        /// Supported languages for transcripts
        static let supportedLanguages: [(code: String, name: String)] = [
            ("en", "English"),
            ("pt", "Portuguese"),
            ("es", "Spanish"),
            ("fr", "French"),
            ("de", "German"),
            ("it", "Italian"),
            ("ja", "Japanese"),
            ("ko", "Korean"),
            ("zh", "Chinese")
        ]

        /// Default transcript language
        static let defaultLanguage = "en"
    }

    // MARK: - AI

    enum AI {
        /// Maximum tokens for transcript processing
        static let maxTranscriptTokens = 6000

        /// Estimated characters per token
        static let charsPerToken = 4

        /// Maximum characters for transcript (~24000)
        static var maxTranscriptChars: Int { maxTranscriptTokens * charsPerToken }

        /// Truncation ratio (60% beginning, 40% end)
        static let beginningRatio = 0.6

        /// Default max tokens for AI response
        static let defaultMaxResponseTokens = 1000

        /// Temperature for AI generation
        static let defaultTemperature = 0.7
    }

    // MARK: - Logging

    enum Logging {
        /// Buffer size before flushing to database
        static let bufferSize = 50

        /// Maximum recent logs to keep in memory
        static let maxRecentLogs = 500

        /// Days to keep logs before cleanup
        static let logRetentionDays = 7

        /// Analytics buffer size
        static let analyticsBufferSize = 20
    }

    // MARK: - UI

    enum UI {
        /// Default font size for reading
        static let defaultFontSize: Double = 16

        /// Font size range
        static let minFontSize: Double = 12


Let me check the AIService and WhisperService to understand async patterns:


     1→import Foundation
     2→
     3→// MARK: - Summary Style
     4→
     5→enum SummaryStyle: String, CaseIterable, Identifiable {
     6→    case bullets = "bullets"
     7→    case paragraph = "paragraph"
     8→    case keyPoints = "keyPoints"
     9→
    10→    var id: String { rawValue }
    11→
    12→    var displayName: String {
    13→        switch self {
    14→        case .bullets: return "Bullet Points"
    15→        case .paragraph: return "Paragraph"
    16→        case .keyPoints: return "Key Points (5)"
    17→        }
    18→    }
    19→
    20→    var systemPrompt: String {
    21→        switch self {
    22→        case .bullets:
    23→            return "You are a helpful assistant that summarizes video transcripts. Create concise, well-organized bullet points. Respond in the same language as the transcript."
    24→        case .paragraph:
    25→            return "You are a helpful assistant that summarizes video transcripts. Write clear, flowing paragraphs. Respond in the same language as the transcript."
    26→        case .keyPoints:
    27→            return "You are a helpful assistant that extracts key takeaways from video transcripts. Focus on actionable insights. Respond in the same language as the transcript."
    28→        }
    29→    }
    30→
    31→    var userPrompt: String {
    32→        switch self {
    33→        case .bullets:
    34→            return "Summarize this video transcript in 5-7 bullet points. Be concise and focus on the main ideas:\n\n"
    35→        case .paragraph:
    36→            return "Write a 2-3 paragraph summary of this video transcript. Include the main topic, key arguments, and conclusions:\n\n"
    37→        case .keyPoints:
    38→            return "Extract the 5 most important takeaways from this transcript. Format as a numbered list:\n\n"
    39→        }
    40→    }
    41→}
    42→
    43→// MARK: - AI Provider
    44→
    45→enum AIProvider: String, CaseIterable, Identifiable {
    46→    case claude = "claude"
    47→    case lmStudio = "lmstudio"
    48→
    49→    var id: String { rawValue }
    50→
    51→    var displayName: String {
    52→        switch self {
    53→        case .claude: return "Claude (Anthropic)"
    54→        case .lmStudio: return "LM Studio (Local)"
    55→        }
    56→    }
    57→}
    58→
    59→// MARK: - Claude Model
    60→
    61→enum ClaudeModel: String, CaseIterable, Identifiable {
    62→    case sonnet = "claude-sonnet-4-20250514"
    63→    case haiku = "claude-3-haiku-20240307"
    64→
    65→    var id: String { rawValue }
    66→
    67→    var displayName: String {
    68→        switch self {
    69→        case .sonnet: return "Claude Sonnet 4 (Best)"
    70→        case .haiku: return "Claude 3 Haiku (Fast)"
    71→        }
    72→    }
    73→
    74→    var maxTokens: Int {
    75→        switch self {
    76→        case .sonnet: return 8192
    77→        case .haiku: return 4096
    78→        }
    79→    }
    80→}
    81→
    82→// MARK: - AI Error
    83→
    84→enum AIError: LocalizedError {
    85→    case connectionFailed
    86→    case invalidResponse
    87→    case serverError(String)
    88→    case noModelsAvailable
    89→    case requestFailed(String)
    90→    case apiKeyMissing
    91→    case rateLimited
    92→
    93→    var errorDescription: String? {
    94→        switch self {
    95→        case .connectionFailed:
    96→            return "Cannot connect to AI service. Check your internet connection."
    97→        case .invalidResponse:
    98→            return "Invalid response from AI server"
    99→        case .serverError(let msg):
   100→            return "Server error: \(msg)"

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation

// MARK: - Summary Style

enum SummaryStyle: String, CaseIterable, Identifiable {
    case bullets = "bullets"
    case paragraph = "paragraph"
    case keyPoints = "keyPoints"

    var id: String { rawValue }

    var displayName: String {
        switch self {
        case .bullets: return "Bullet Points"
        case .paragraph: return "Paragraph"
        case .keyPoints: return "Key Points (5)"
        }
    }

    var systemPrompt: String {
        switch self {
        case .bullets:
            return "You are a helpful assistant that summarizes video transcripts. Create concise, well-organized bullet points. Respond in the same language as the transcript."
        case .paragraph:
            return "You are a helpful assistant that summarizes video transcripts. Write clear, flowing paragraphs. Respond in the same language as the transcript."
        case .keyPoints:
            return "You are a helpful assistant that extracts key takeaways from video transcripts. Focus on actionable insights. Respond in the same language as the transcript."
        }
    }

    var userPrompt: String {
        switch self {
        case .bullets:
            return "Summarize this video transcript in 5-7 bullet points. Be concise and focus on the main ideas:\n\n"
        case .paragraph:
            return "Write a 2-3 paragraph summary of this video transcript. Include the main topic, key arguments, and conclusions:\n\n"
        case .keyPoints:
            return "Extract the 5 most important takeaways from this transcript. Format as a numbered list:\n\n"
        }
    }
}

// MARK: - AI Provider

enum AIProvider: String, CaseIterable, Identifiable {
    case claude = "claude"
    case lmStudio = "lmstudio"

    var id: String { rawValue }

    var displayName: String {
        switch self {
        case .claude: return "Claude (Anthropic)"
        case .lmStudio: return "LM Studio (Local)"
        }
    }
}

// MARK: - Claude Model

enum ClaudeModel: String, CaseIterable, Identifiable {
    case sonnet = "claude-sonnet-4-20250514"
    case haiku = "claude-3-haiku-20240307"

    var id: String { rawValue }

    var displayName: String {
        switch self {
        case .sonnet: return "Claude Sonnet 4 (Best)"
        case .haiku: return "Claude 3 Haiku (Fast)"
        }
    }

    var maxTokens: Int {
        switch self {
        case .sonnet: return 8192
        case .haiku: return 4096
        }
    }
}

// MARK: - AI Error

enum AIError: LocalizedError {
    case connectionFailed
    case invalidResponse
    case serverError(String)
    case noModelsAvailable
    case requestFailed(String)
    case apiKeyMissing
    case rateLimited

    var errorDescription: String? {
        switch self {
        case .connectionFailed:
            return "Cannot connect to AI service. Check your internet connection."
        case .invalidResponse:
            return "Invalid response from AI server"
        case .serverError(let msg):
            return "Server error: \(msg)"


     1→import Foundation
     2→
     3→/// Service for transcribing audio files using OpenAI Whisper (local)
     4→actor WhisperService {
     5→    static let shared = WhisperService()
     6→
     7→    private init() {}
     8→
     9→    /// Whisper model sizes with different accuracy/speed trade-offs
    10→    enum ModelSize: String, Codable {
    11→        case tiny      // ~1GB RAM, fastest, less accurate
    12→        case base      // ~1GB RAM, fast, decent accuracy
    13→        case small     // ~2GB RAM, balanced
    14→        case medium    // ~5GB RAM, good accuracy
    15→        case large     // ~10GB RAM, best accuracy
    16→
    17→        var description: String {
    18→            switch self {
    19→            case .tiny: return "Tiny (rápido, menos preciso)"
    20→            case .base: return "Base (balanceado)"
    21→            case .small: return "Small (preciso, mais lento)"
    22→            case .medium: return "Medium (muito preciso)"
    23→            case .large: return "Large (melhor qualidade)"
    24→            }
    25→        }
    26→    }
    27→
    28→    /// Transcribes audio file using Whisper
    29→    /// - Parameters:
    30→    ///   - audioPath: Path to audio file (MP3, WAV, etc.)
    31→    ///   - language: Optional language hint (e.g. "portuguese", "english")
    32→    ///   - modelSize: Whisper model size to use
    33→    ///   - progress: Progress callback with status messages
    34→    /// - Returns: Transcribed text
    35→    func transcribe(
    36→        audioPath: URL,
    37→        language: String? = nil,
    38→        modelSize: ModelSize = .base,
    39→        progress: @escaping @Sendable (String) -> Void
    40→    ) async throws -> String {
    41→        await MainActor.run { progress("🤖 Transcrevendo áudio com Whisper (\(modelSize.rawValue))...") }
    42→        await AppLogger.shared.info("Starting Whisper transcription", category: .transcript, metadata: [
    43→            "audio_path": audioPath.path,
    44→            "model": modelSize.rawValue,
    45→            "language": language ?? "auto"
    46→        ])
    47→
    48→        let script = """
    49→        import sys
    50→        sys.stdout.reconfigure(line_buffering=True)
    51→        import whisper
    52→        import os
    53→
    54→        # Add ffmpeg to PATH for Whisper
    55→        os.environ["PATH"] = "\(AppConfig.Executables.ffmpegDirectory):" + os.environ.get("PATH", "")
    56→
    57→        audio_path = '\(audioPath.path.replacingOccurrences(of: "'", with: "\\'"))'
    58→        model_size = '\(modelSize.rawValue)'
    59→        language = \(language != nil ? "'\(language!)'" : "None")
    60→
    61→        try:
    62→            print("PROGRESS: Carregando modelo Whisper...", flush=True)
    63→            model = whisper.load_model(model_size)
    64→
    65→            print("PROGRESS: Transcrevendo áudio (pode demorar alguns minutos)...", flush=True)
    66→
    67→            # Transcribe with optional language hint
    68→            options = {}
    69→            if language:
    70→                options["language"] = language
    71→
    72→            result = model.transcribe(audio_path, **options, verbose=False)
    73→
    74→            print("PROGRESS: Transcrição concluída!", flush=True)
    75→            print("TRANSCRIPT_START")
    76→            print(result["text"])
    77→            print("TRANSCRIPT_END")
    78→
    79→            # Print detected language
    80→            if "language" in result:
    81→                print(f"DETECTED_LANGUAGE: {result['language']}")
    82→        except Exception as e:
    83→            import traceback
    84→            print(f"ERROR: {str(e)}")
    85→            print(traceback.format_exc(), file=sys.stderr)
    86→        """
    87→
    88→        let output = try await runPythonWithProgress(
    89→            script: script,
    90→            timeout: 3600, // 1 hour for long videos
    91→            progress: progress
    92→        )
    93→
    94→        if output.contains("ERROR:") {
    95→            let errorMsg = output.replacingOccurrences(of: "ERROR: ", with: "")
    96→            await AppLogger.shared.error("Whisper transcription failed: \(errorMsg)", category: .transcript)
    97→            throw WhisperError.transcriptionFailed(errorMsg)
    98→        }
    99→
   100→        // Extract transcript

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation

/// Service for transcribing audio files using OpenAI Whisper (local)
actor WhisperService {
    static let shared = WhisperService()

    private init() {}

    /// Whisper model sizes with different accuracy/speed trade-offs
    enum ModelSize: String, Codable {
        case tiny      // ~1GB RAM, fastest, less accurate
        case base      // ~1GB RAM, fast, decent accuracy
        case small     // ~2GB RAM, balanced
        case medium    // ~5GB RAM, good accuracy
        case large     // ~10GB RAM, best accuracy

        var description: String {
            switch self {
            case .tiny: return "Tiny (rápido, menos preciso)"
            case .base: return "Base (balanceado)"
            case .small: return "Small (preciso, mais lento)"
            case .medium: return "Medium (muito preciso)"
            case .large: return "Large (melhor qualidade)"
            }
        }
    }

    /// Transcribes audio file using Whisper
    /// - Parameters:
    ///   - audioPath: Path to audio file (MP3, WAV, etc.)
    ///   - language: Optional language hint (e.g. "portuguese", "english")
    ///   - modelSize: Whisper model size to use
    ///   - progress: Progress callback with status messages
    /// - Returns: Transcribed text
    func transcribe(
        audioPath: URL,
        language: String? = nil,
        modelSize: ModelSize = .base,
        progress: @escaping @Sendable (String) -> Void
    ) async throws -> String {
        await MainActor.run { progress("🤖 Transcrevendo áudio com Whisper (\(modelSize.rawValue))...") }
        await AppLogger.shared.info("Starting Whisper transcription", category: .transcript, metadata: [
            "audio_path": audioPath.path,
            "model": modelSize.rawValue,
            "language": language ?? "auto"
        ])

        let script = """
        import sys
        sys.stdout.reconfigure(line_buffering=True)
        import whisper
        import os

        # Add ffmpeg to PATH for Whisper
        os.environ["PATH"] = "\(AppConfig.Executables.ffmpegDirectory):" + os.environ.get("PATH", "")

        audio_path = '\(audioPath.path.replacingOccurrences(of: "'", with: "\\'"))'
        model_size = '\(modelSize.rawValue)'
        language = \(language != nil ? "'\(language!)'" : "None")

        try:
            print("PROGRESS: Carregando modelo Whisper...", flush=True)
            model = whisper.load_model(model_size)

            print("PROGRESS: Transcrevendo áudio (pode demorar alguns minutos)...", flush=True)

            # Transcribe with optional language hint
            options = {}
            if language:
                options["language"] = language

            result = model.transcribe(audio_path, **options, verbose=False)

            print("PROGRESS: Transcrição concluída!", flush=True)
            print("TRANSCRIPT_START")
            print(result["text"])
            print("TRANSCRIPT_END")

            # Print detected language
            if "language" in result:
                print(f"DETECTED_LANGUAGE: {result['language']}")
        except Exception as e:
            import traceback
            print(f"ERROR: {str(e)}")
            print(traceback.format_exc(), file=sys.stderr)
        """

        let output = try await runPythonWithProgress(
            script: script,
            timeout: 3600, // 1 hour for long videos
            progress: progress
        )

        if output.contains("ERROR:") {
            let errorMsg = output.replacingOccurrences(of: "ERROR: ", with: "")
            await AppLogger.shared.error("Whisper transcription failed: \(errorMsg)", category: .transcript)
            throw WhisperError.transcriptionFailed(errorMsg)
        }

        // Extract transcript


Perfect! Now I have a comprehensive understanding of the project. Let me create a detailed exploration report.

## YouTubeTranscripts-macOS Project Exploration Report

I've thoroughly explored the YouTubeTranscripts-macOS project. Here's what I found:

---

### **1. TRANSCRIPT FETCHING ARCHITECTURE**

#### **TranscriptInputViewModel** (Main Entry Point)
**Location:** `/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/Sources/ViewModels/TranscriptInputViewModel.swift`

- **State Management:**
  - `@Published var youtubeURL: String` - User input
  - `@Published var selectedLanguage: String` - Language selection
  - `@Published var includeTimestamps: Bool` - Options
  - `@Published var isLoading: Bool` - Loading state
  - `@Published var logs: [InputLogEntry]` - User-facing logs
  - `@Published var currentStep: TranscriptStep` - Progress tracking (6 steps)
  - `@Published var elapsedTime: Int` - Timer for UI feedback

- **Flow Steps (TranscriptStep enum):**
  1. `idle` - Ready to fetch
  2. `validating` - Validating URL
  3. `fetchingInfo` - Getting video metadata
  4. `fetchingTranscript` - Downloading transcript
  5. `processing` - Text processing
  6. `saving` - Database save
  7. `complete` - Success
  8. `failed` - Error state

- **Main Method: `fetchTranscript(modelContext, onSave)`**
  - Validates URL → Gets video info → Fetches transcript → Processes → Saves → Callback
  - Includes progress timer (logs every 30 seconds)
  - Comprehensive error handling with user-friendly messages

---

### **2. TRANSCRIPT FETCHING FLOW (MCPService)**

**Location:** `/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/Sources/Services/MCPService.swift`

#### **Main Methods:**

**A. `getTranscriptWithWhisperFallback()` - Two-Tier Strategy**
```
Tier 1: Try YouTube Captions
  ├─ List available languages
  ├─ Build language priority list
  └─ Try each language in order

Tier 2: If no captions → Whisper transcription
  ├─ Download audio (pytubefix/yt-dlp)
  ├─ Transcribe with Whisper (base model)
  └─ Cleanup temp files
```

**B. `getTranscriptWithProgress()` - Smart Language Detection**
- Attempts languages in priority order:
  1. User's preferred language (if available)
  2. Portuguese variants (pt, pt-BR, pt-PT)
  3. English variants (en, en-US, en-GB, en-AU)
  4. Spanish variants
  5. French variants
  6. Any other available languages

**C. `fetchTranscriptInLanguage()` - Low-Level Fetcher**
- Uses Python script to call `youtube_transcript_api`
- Includes real-time progress markers (`PROGRESS:` prefix)
- Parses transcript with `TRANSCRIPT_START/END` markers
- Timeout support (10 minutes default)

#### **Progress Callback System:**
```swift
progress: @escaping @Sendable (String) -> Void
```
- Called from UI thread via `@MainActor.run`
- Messages like: "📡 Conectando à API do YouTube..."
- Parsed from Python output lines starting with "PROGRESS:"

---

### **3. DATA PERSISTENCE (SavedVideo Model)**

**Location:** `/Users/neog/Apps/macOS/YouTubeTranscripts-macOS/Sources/Models/SavedVideo.swift`

```swift
@Model final class SavedVideo {
    var id: UUID
    var youtubeURL: String
    var videoID: String
    var title: String
    var channelName: String
    var transcript: String        // Raw transcript
    var language: String
    var duration: String?
    var thumbnailURL: String?
    var savedAt: Date
    var isFavorite: Bool

    // Annotations (stored as JSON in Data field)
    var annotationsData: Data?

    // AI Features
    var notes: String = ""
    var aiSummary: String?
    var aiSummaryGeneratedAt: Date?

    // AI Formatting
    var formattedTranscript: String?
    var formattedAt: Date?
    var formattingMode: String?

    // Source tracking
    var transcriptSource: String?  // "youtube" or "whisper"
}
```

- **Persistence:** SwiftData (Apple's modern ORM)
- **Query:** `@Query(sort: \SavedVideo.savedAt, order: .reverse)`
- **Unique constraint:** `videoID` prevents duplicates

---

### **4. ASYNC/CONCURRENCY PATTERNS**

#### **Actor-Based Services:**
- `MCPService` - Singleton actor for transcript fetching
- `WhisperService` - Actor for audio transcription
- Thread-safe by design

#### **ProcessDataBuffer Actor:**
```swift
private actor ProcessDataBuffer {
    private var data = Data()
    private var isCompleted = false

    func append(_ newData: Data)
    func getData() -> Data
    func markCompleted() -> Bool  // Atomic flag
}
```

#### **Progress Reading (Real-time Output):**
```swift
outputPipe.fileHandleForReading.readabilityHandler = { handle in
    let data = handle.availableData
    Task { await buffer.append(data) }  // Thread-safe append
    // Parse PROGRESS: lines and dispatch to MainActor
}
```

#### **Timeout Handling:**
```swift
let timeoutTimer = DispatchSource.makeTimerSource(queue: .global())
timeoutTimer.schedule(deadline: .now() + timeout)
timeoutTimer.setEventHandler {
    Task {
        let alreadyCompleted = await buffer.checkCompleted()
        if !alreadyCompleted {
            process.terminate()
            continuation.resume(throwing: MCPError.timeout)
        }
    }
}
```

---

### **5. QUEUE/BATCH PROCESSING PATTERNS**

**Current Implementation: Single-URL Sequential**
- No built-in batch processor
- One URL at a time via `fetchTranscript()`
- Progress visible in UI logs

**Potential Extension Points for Batch:**

1. **TranscriptImporter (Single-Transcript Importer)**
   ```swift
   struct TranscriptImporter {
       static func importTranscript(...)  // Single import
       static func importPalantirTranscript(...)  // Specific file
   }
   ```
   - Currently handles one import at a time
   - Could be extended to `importTranscripts(_ videos: [VideoMetadata])`

2. **Database Persistence Ready:**
   - SwiftData handles concurrent writes
   - ModelContext can insert multiple SavedVideo objects
   - Batch transactions possible with `modelContext.insert()` + `save()`

3. **Processing Pipeline Pattern:**
   ```
   Input Queue → Validate → Fetch Info → Get Transcript → Process → Save
   ```
   - Each stage produces output for next stage
   - Could buffer inputs and process asynchronously

---

### **6. KEY SERVICE LAYERS**

| Service | Purpose | Pattern |
|---------|---------|---------|
| **MCPService** | YouTube API wrapper | Actor, Python subprocess |
| **WhisperService** | Audio transcription | Actor, Python subprocess, 1-hour timeout |
| **AudioDownloadService** | Video→Audio conversion | Downloads via pytubefix/yt-dlp |
| **AIService** | Summarization & formatting | LM Studio integration |
| **TTSService** | Text-to-speech | AVFoundation wrapper |
| **SearchService** | Transcript search | In-memory regex |

---

### **7. ERROR HANDLING**

**MCPError Enum:**
- `processError(String)` - Python subprocess failure
- `invalidURL` - URL validation failed
- `transcriptNotAvailable(String)` - No captions available
- `timeout` - 10-minute limit exceeded
- `noTranscriptsAvailable(videoID)` - Video has no captions
- `allLanguagesFailed([String], Error?)` - Tried all languages

**User-Facing Messages (Portuguese):**
- "❌ Nenhuma legenda disponível"
- "⚠ Timeout: operação excedeu o tempo limite"
- Friendly tooltips and retry suggestions

---

### **8. UI/UX INTEGRATION**

**ContentView → TranscriptInputView → TranscriptInputViewModel → MCPService**

- **Notification System:**
  - `.newTranscript` - Open input form
  - `.deleteVideo` - Confirm deletion
  - `.importWhisperTranscript` - Batch import trigger

- **Progress Display:**
  - Step indicator (6-step process)
  - Real-time logs with color-coded types
  - Copy logs to clipboard button
  - Elapsed time timer

- **Callbacks:**
  ```swift
  onSave: @escaping (SavedVideo) -> Void
  ```
  - Updates sidebar, switches to reading view

---

### **9. RECOMMENDED BATCH PROCESSING ARCHITECTURE**

For implementing URL queue processing, I'd recommend:

1. **Create `BatchFetchQueue` Actor:**
   - Maintain queue of VideoMetadata
   - Process sequentially to avoid API throttling
   - Publish progress events

2. **Extend TranscriptInputViewModel:**
   - Add batch mode toggle
   - Support multi-line URL input
   - Show progress per-item

3. **Leverage Existing Patterns:**
   - Use actor pattern (already used)
   - Reuse progress callbacks
   - Use existing error handling

4. **Database Optimization:**
   - Batch insert 10 videos per transaction
   - Keep ModelContext lifecycle scoped

---

### **KEY FILE PATHS**

| File | Purpose |
|------|---------|
| `/Sources/ViewModels/TranscriptInputViewModel.swift` | Main flow controller |
| `/Sources/Services/MCPService.swift` | Transcript fetcher (848 lines) |
| `/Sources/Services/WhisperService.swift` | Audio transcription |
| `/Sources/Models/SavedVideo.swift` | Data model |
| `/Sources/Core/TranscriptImporter.swift` | Import utility |
| `/Sources/Views/TranscriptInputView.swift` | Input UI |
| `/Sources/Core/AppConfiguration.swift` | Global config |

---

This is a well-architected project with clear separation of concerns, actor-based thread safety, and extensive progress feedback. The foundation is solid for adding batch processing while maintaining the existing single-URL flow.