@@ -2,6 +2,7 @@ import SwiftUI
22
33struct ModelContent : View {
44 @ObservedObject var viewModel : SettingsViewModel
5+ @State private var showModelDownloadSheet = false
56
67 var body : some View {
78 VStack ( spacing: 20 ) {
@@ -46,13 +47,242 @@ struct ModelContent: View {
4647 icon: " arrow.down.circle.fill "
4748 ) {
4849 VStack ( alignment: . leading, spacing: 12 ) {
49- Text ( " Add GGML format models to the folder above, then restart the app ." )
50+ Text ( " Choose from pre-configured models or add your own GGML format models ." )
5051 . font ( . system( size: 13 ) )
5152 . foregroundColor ( . secondary)
5253
53- Link ( " Browse Available Models " , destination: URL ( string: " https://huggingface.co/ggerganov/whisper.cpp/tree/main " ) !)
54- . font ( . system( size: 13 , weight: . medium) )
55- . foregroundColor ( . accentColor)
54+ HStack {
55+ Button ( " Download Models " ) {
56+ showModelDownloadSheet = true
57+ }
58+ . buttonStyle ( GlassButtonStyle ( ) )
59+
60+ Link ( " Browse HuggingFace " , destination: URL ( string: " https://huggingface.co/ggerganov/whisper.cpp/tree/main " ) !)
61+ . font ( . system( size: 13 , weight: . medium) )
62+ . foregroundColor ( . accentColor)
63+ }
64+ }
65+ }
66+ }
67+ . sheet ( isPresented: $showModelDownloadSheet) {
68+ ModelDownloadView ( onDismiss: {
69+ showModelDownloadSheet = false
70+ viewModel. loadAvailableModels ( ) // Refresh models after download
71+ } )
72+ . frame ( width: 600 , height: 500 )
73+ }
74+ }
75+ }
76+
77+ struct ModelDownloadView : View {
78+ let onDismiss : ( ) -> Void
79+ @StateObject private var viewModel = ModelDownloadViewModel ( )
80+
81+ var body : some View {
82+ VStack ( spacing: 20 ) {
83+ // Header
84+ HStack {
85+ VStack ( alignment: . leading, spacing: 4 ) {
86+ Text ( " Download Whisper Models " )
87+ . font ( . title2)
88+ . fontWeight ( . bold)
89+
90+ Text ( " Select a model to download " )
91+ . font ( . subheadline)
92+ . foregroundColor ( . secondary)
93+ }
94+
95+ Spacer ( )
96+
97+ Button ( " Done " ) {
98+ onDismiss ( )
99+ }
100+ . buttonStyle ( GlassButtonStyle ( ) )
101+ }
102+ . padding ( . horizontal)
103+ . padding ( . top)
104+
105+ // Model List
106+ ScrollView {
107+ VStack ( spacing: 12 ) {
108+ ForEach ( $viewModel. models) { $model in
109+ ModelDownloadCard ( model: $model, viewModel: viewModel)
110+ }
111+ }
112+ . padding ( )
113+ }
114+ }
115+ . background ( Color ( . windowBackgroundColor) )
116+ }
117+ }
118+
119+ struct ModelDownloadCard : View {
120+ @Binding var model : DownloadableModel
121+ @ObservedObject var viewModel : ModelDownloadViewModel
122+ @State private var isHovered = false
123+
124+ var body : some View {
125+ VStack ( spacing: 12 ) {
126+ HStack ( spacing: 16 ) {
127+ // Model info
128+ VStack ( alignment: . leading, spacing: 4 ) {
129+ HStack ( spacing: 8 ) {
130+ Text ( model. name)
131+ . font ( . system( size: 16 , weight: . semibold) )
132+ . foregroundColor ( . primary)
133+
134+ if model. name == " Turbo V3 large " {
135+ Text ( " RECOMMENDED " )
136+ . font ( . system( size: 9 , weight: . bold) )
137+ . foregroundColor ( . black)
138+ . padding ( . horizontal, 6 )
139+ . padding ( . vertical, 2 )
140+ . background (
141+ Capsule ( )
142+ . fill ( Color . mint)
143+ )
144+ }
145+
146+ Spacer ( )
147+ }
148+
149+ Text ( model. sizeString)
150+ . font ( . system( size: 13 ) )
151+ . foregroundColor ( . secondary)
152+ }
153+
154+ // Status indicator
155+ Group {
156+ if model. isDownloaded {
157+ Image ( systemName: " checkmark.circle.fill " )
158+ . font ( . system( size: 20 ) )
159+ . foregroundColor ( . mint)
160+ } else if model. downloadProgress > 0 && model. downloadProgress < 1 {
161+ ZStack {
162+ Circle ( )
163+ . stroke ( Color . gray. opacity ( 0.3 ) , lineWidth: 2 )
164+ . frame ( width: 20 , height: 20 )
165+
166+ Circle ( )
167+ . trim ( from: 0 , to: model. downloadProgress)
168+ . stroke ( Color . mint, lineWidth: 2 )
169+ . frame ( width: 20 , height: 20 )
170+ . rotationEffect ( . degrees( - 90 ) )
171+ }
172+ } else {
173+ Button ( action: {
174+ viewModel. downloadModel ( model)
175+ } ) {
176+ Image ( systemName: " arrow.down.circle " )
177+ . font ( . system( size: 20 ) )
178+ . foregroundColor ( . accentColor)
179+ }
180+ . buttonStyle ( . plain)
181+ . disabled ( viewModel. isDownloadingAny)
182+ }
183+ }
184+ }
185+
186+ // Performance bars
187+ HStack ( spacing: 24 ) {
188+ PerformanceBar ( title: " Accuracy " , value: model. accuracyRate, color: . mint)
189+ PerformanceBar ( title: " Speed " , value: model. speedRate, color: . orange)
190+ }
191+ }
192+ . padding ( 16 )
193+ . background (
194+ RoundedRectangle ( cornerRadius: 12 )
195+ . fill ( Color ( . controlBackgroundColor) . opacity ( 0.6 ) )
196+ . overlay (
197+ RoundedRectangle ( cornerRadius: 12 )
198+ . stroke ( Color . gray. opacity ( 0.15 ) , lineWidth: 1 )
199+ )
200+ )
201+ . scaleEffect ( isHovered ? 1.02 : 1.0 )
202+ . animation ( . easeInOut( duration: 0.2 ) , value: isHovered)
203+ . onHover { hovering in
204+ isHovered = hovering
205+ }
206+ }
207+ }
208+
209+ struct PerformanceBar : View {
210+ let title : String
211+ let value : Int
212+ let color : Color
213+
214+ var body : some View {
215+ VStack ( spacing: 6 ) {
216+ Text ( title)
217+ . font ( . system( size: 10 , weight: . medium) )
218+ . foregroundColor ( . secondary)
219+
220+ ZStack ( alignment: . leading) {
221+ RoundedRectangle ( cornerRadius: 2 )
222+ . fill ( Color . gray. opacity ( 0.2 ) )
223+ . frame ( width: 50 , height: 3 )
224+
225+ RoundedRectangle ( cornerRadius: 2 )
226+ . fill ( color)
227+ . frame ( width: 50 * Double( value) / 100 , height: 3 )
228+ }
229+
230+ Text ( " \( value) % " )
231+ . font ( . system( size: 9 , weight: . medium, design: . monospaced) )
232+ . foregroundColor ( . secondary)
233+ }
234+ }
235+ }
236+
237+ class ModelDownloadViewModel : ObservableObject {
238+ @Published var models : [ DownloadableModel ] = [ ]
239+ @Published var isDownloadingAny : Bool = false
240+
241+ private let modelManager = WhisperModelManager . shared
242+
243+ init ( ) {
244+ initializeModels ( )
245+ }
246+
247+ private func initializeModels( ) {
248+ // Initialize models with their actual download status
249+ models = availableModels. map { model in
250+ var updatedModel = model
251+ updatedModel. isDownloaded = modelManager. isModelDownloaded ( name: model. url. lastPathComponent)
252+ return updatedModel
253+ }
254+ }
255+
256+ func downloadModel( _ model: DownloadableModel ) {
257+ guard !model. isDownloaded && !isDownloadingAny else { return }
258+
259+ isDownloadingAny = true
260+
261+ // Find the index of the model we're downloading
262+ guard let modelIndex = models. firstIndex ( where: { $0. id == model. id } ) else {
263+ isDownloadingAny = false
264+ return
265+ }
266+
267+ Task {
268+ do {
269+ // Start the download with progress updates
270+ let filename = model. url. lastPathComponent
271+
272+ try await modelManager. downloadModel ( url: model. url, name: filename) { [ weak self] progress in
273+ DispatchQueue . main. async {
274+ self ? . models [ modelIndex] . downloadProgress = progress
275+ if progress >= 1.0 {
276+ self ? . models [ modelIndex] . isDownloaded = true
277+ self ? . isDownloadingAny = false
278+ }
279+ }
280+ }
281+ } catch {
282+ print ( " Failed to download model: \( error) " )
283+ DispatchQueue . main. async {
284+ self . models [ modelIndex] . downloadProgress = 0
285+ self . isDownloadingAny = false
56286 }
57287 }
58288 }
0 commit comments