spb/forge-studio Public
The Instruments of LLM training — a native macOS cockpit for Forge. Train language models from scratch on Apple Silicon without a terminal.
Swift 95.7%
Shell 4.3%
1// Author: Simon-Pierre Boucher — contact@spboucher.ai2//3// Forge Studio — GUI companion for the Forge LLM training framework.4import SwiftUI56@main7struct ForgeStudioApp: App {8 @State private var model = AppModel()910 var body: some Scene {11 WindowGroup("Forge Studio") {12 ContentView()13 .environment(model)14 .frame(minWidth: 980, minHeight: 640)15 }16 Settings {17 SettingsView()18 .environment(model)19 }20 }21}2223@Observable24@MainActor25final class AppModel {26 var store: RunStore27 var supervisor: RunSupervisor28 var forgeDevice: String?29 var forgeError: String?3031 init() {32 let workspace = ForgeBinaryLocator.savedWorkspaceURL33 ?? FileManager.default.homeDirectoryForCurrentUser34 .appendingPathComponent("ForgeStudioWorkspace")35 let store = RunStore(workspace: workspace)36 self.store = store37 self.supervisor = RunSupervisor(store: store)38 recoverOrphans()39 RunSupervisor.requestNotificationAuth()40 Task { await self.validateForge() }41 }4243 /// Crash recovery: a run persisted as active means the app died mid-run.44 /// If its forge PID is still alive we leave the process alone and record45 /// the truth (stopped tracking); if it's gone, the run is marked stopped46 /// with an honest reason. A run never stays in a lying state.47 private func recoverOrphans() {48 for var run in store.runs where run.state.isActive {49 if let pid = run.pid, kill(pid, 0) == 0 {50 run.failureReason =51 "L'app a quitté pendant le run ; forge (pid \(pid)) tourne encore — "52 + "métriques reprises depuis log.csv, contrôle du process perdu."53 } else {54 run.failureReason =55 "L'app a quitté pendant le run ; forge n'est plus actif. "56 + "Reprise possible depuis ckpt_latest.bin."57 }58 run.state = .stopped59 run.pid = nil60 store.upsert(run)61 }62 }6364 func validateForge() async {65 guard let binary = ForgeBinaryLocator.savedBinaryURL66 ?? ForgeBinaryLocator.candidates().first else {67 forgeError = "Binaire forge introuvable — choisissez-le dans les Réglages."68 return69 }70 switch await ForgeBinaryLocator.validate(binary: binary) {71 case .success(let v):72 forgeDevice = v.version73 forgeError = nil74 ForgeBinaryLocator.save(binary: binary)75 case .failure(let e):76 forgeError = e.localizedDescription77 }78 }7980 func datasets() -> [Dataset] {81 let dataDir = store.workspaceURL.appendingPathComponent("data")82 let forgeData = ForgeBinaryLocator.savedBinaryURL?83 .deletingLastPathComponent().deletingLastPathComponent()84 .appendingPathComponent("data")85 var found: [Dataset] = []86 for root in [dataDir, forgeData].compactMap({ $0 }) {87 guard let subdirs = try? FileManager.default.contentsOfDirectory(88 at: root, includingPropertiesForKeys: nil) else { continue }89 for dir in subdirs {90 if let ds = Dataset.scan(directory: dir),91 !found.contains(where: { $0.path == ds.path }) {92 found.append(ds)93 }94 }95 }96 return found.sorted { $0.name < $1.name }97 }98}99