import ArgumentParser import Foundation import ForgeCamera import ForgeCameraARRI import ForgeGrade import ForgeColor import ForgeSim @main struct Forge: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "forge", abstract: "On-set live grading: push looks to cameras over IP.", subcommands: [Push.self, Status.self, Sim.self]) } struct CameraOptions: ParsableArguments { @Option(name: .long, help: "Camera IP/host") var host: String @Option(name: .long, help: "Camera port") var port: Int @Option(name: .long, help: "Driver: arri") var driver: String = "arri" func makeDriver() throws -> any CameraDriver { switch driver { case "arri": return ArriDriver(host: host, port: port) default: throw ValidationError("unknown driver '\(driver)' (supported: arri)") } } } struct Push: AsyncParsableCommand { static let configuration = CommandConfiguration(abstract: "Flatten a grade stack and push to camera.") @OptionGroup var camera: CameraOptions @Option(name: .long, help: "Grade snapshot JSON file") var look: String @Option(name: .long, help: "LUT lattice size") var lattice: Int = 33 @Flag(name: .long, help: "Keep leading CDL native (camera trim), bake remainder") var splitCdl = false func run() async throws { let data = try Data(contentsOf: URL(fileURLWithPath: look)) let stack = try GradeSnapshot.decode(data) let driver = try camera.makeDriver() let flattened = Flattener.flatten( stack: stack, latticeSize: lattice, splitLeadingCDL: splitCdl && driver.capabilities.supportsNativeCDL) try await driver.connect() try await driver.push(look: flattened) await driver.disconnect() print("pushed: lattice=\(lattice) cdl=\(flattened.cdl != nil ? "native" : "baked") -> \(camera.host):\(camera.port)") } } struct Status: AsyncParsableCommand { static let configuration = CommandConfiguration(abstract: "Print camera connection/rec state and metadata.") @OptionGroup var camera: CameraOptions func run() async throws { let driver = try camera.makeDriver() var iterator = driver.state.makeAsyncIterator() try await driver.connect() // First polled state carries metadata. var reported = false for _ in 0..<10 { guard let s = await iterator.next() else { break } if let md = s.metadata { print("connection: \(s.connection.rawValue)") print("recording: \(s.isRecording)") print("clip: \(s.clipName ?? "-")") print("ei: \(md.exposureIndex.map(String.init) ?? "-") wb: \(md.whiteBalance.map(String.init) ?? "-") tint: \(md.tint.map(String.init) ?? "-")") print("fps: \(md.fps.map { String($0) } ?? "-") tc: \(md.timecode ?? "-")") reported = true break } } if !reported { print("connection: connected (no metadata yet)") } await driver.disconnect() } } struct Sim: AsyncParsableCommand { static let configuration = CommandConfiguration(abstract: "Run an ARRI-style camera simulator.") @Option(name: .long, help: "Listen port") var port: Int = 8035 func run() async throws { let sim = ArriSimulator() try await sim.start(port: port) let bound = await sim.boundPort ?? port print("arri sim listening on 127.0.0.1:\(bound)") // Run until killed. while true { try await Task.sleep(for: .seconds(3600)) } } }