Compare commits

...

8 Commits

Author SHA1 Message Date
Christoph Hagen
5cf3c8d0c1 Add image assets 2024-03-01 17:50:33 +01:00
Christoph Hagen
7b3210169a Update training info in readme 2024-03-01 17:20:02 +01:00
Christoph Hagen
e34845ab24 Add comment 2023-12-25 19:05:27 +01:00
Christoph Hagen
400753a9a2 Allow custom data directory 2023-12-25 14:35:01 +01:00
Christoph Hagen
1c57b538be Display environment on startup 2023-12-19 20:45:16 +01:00
Christoph Hagen
5138bb543e Ignore more files 2023-12-19 20:45:02 +01:00
Christoph Hagen
e6132a38b3 Switch to new vapor main 2023-12-06 09:39:12 +01:00
Christoph Hagen
6aaa9cb458 Simplify async scheduler 2023-12-06 09:13:59 +01:00
22 changed files with 115 additions and 114 deletions

13
.gitignore vendored
View File

@@ -8,12 +8,11 @@
.DS_Store .DS_Store
Package.resolved Package.resolved
.swiftpm/ .swiftpm/
Public/caps.json
Public/changes.txt
Public/classifier.*
Public/count.*
Public/images/ Public/images/
Public/thumbnails/ Public/thumbnails/
Public/classifier.version Resources/config.json
Public/classifier.mlmodel Resources/logs/
Public/caps.json
Training/backup/
Training/config.json
Public/thumbnails
Public/count.js

View File

@@ -13,21 +13,15 @@ let package = Package(
.package(url: "https://github.com/christophhagen/ClairvoyantBinaryCodable", from: "0.3.0"), .package(url: "https://github.com/christophhagen/ClairvoyantBinaryCodable", from: "0.3.0"),
], ],
targets: [ targets: [
.target(name: "App", .executableTarget(
dependencies: [ name: "App",
.product(name: "Vapor", package: "vapor"), dependencies: [
.product(name: "Clairvoyant", package: "Clairvoyant"), .product(name: "Vapor", package: "vapor"),
.product(name: "ClairvoyantVapor", package: "ClairvoyantVapor"), .product(name: "Clairvoyant", package: "Clairvoyant"),
.product(name: "ClairvoyantBinaryCodable", package: "ClairvoyantBinaryCodable"), .product(name: "ClairvoyantVapor", package: "ClairvoyantVapor"),
], .product(name: "ClairvoyantBinaryCodable", package: "ClairvoyantBinaryCodable"),
swiftSettings: [ ]
// Enable better optimizations when building in Release configuration. Despite the use of )
// the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
// builds. See <https://github.com/swift-server/guides#building-for-production> for details.
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
]),
.executableTarget(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App"]),
] ]
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/assets/mstile-150x150.png?v=1"/>
<TileColor>#a36490</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
Public/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -5,13 +5,13 @@
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="robots" content="noindex"/> <meta name="robots" content="noindex"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png?v=1"/> <link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png?v=1"/>
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png?v=1"/> <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png?v=1"/>
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png?v=1"/> <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png?v=1"/>
<link rel="manifest" href="/assets/icons/site.webmanifest?v=1"/> <link rel="manifest" href="/assets/site.webmanifest?v=1"/>
<link rel="shortcut icon" href="/assets/icons/favicon.ico?v=1"/> <link rel="shortcut icon" href="/assets/favicon.ico?v=1"/>
<meta name="msapplication-TileColor" content="#da532c"> <meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="/assets/icons/browserconfig.xml?v=1"/> <meta name="msapplication-config" content="/assets/browserconfig.xml?v=1"/>
<meta name="theme-color" content="#ffffff"/> <meta name="theme-color" content="#ffffff"/>
<link href="grid.css?v=2" rel="stylesheet"/> <link href="grid.css?v=2" rel="stylesheet"/>
<meta name="author" content="Christoph Hagen"/> <meta name="author" content="Christoph Hagen"/>

View File

@@ -51,31 +51,7 @@ Note: The data for the mosaic is currently not updated automatically, since Swif
## Classifier training ## Classifier training
The main server is running on Linux, which doesn't provide the CreateML framework required for classifier training. The main server is running on Linux, which doesn't provide the CreateML framework required for classifier training.
This has to be done on macOS using the `train.swift` script in the `Training` folder. This has to be done on macOS using the [Caps-Train](https://christophhagen.de/git/ch/Caps-Train) repository.
It will:
- Fetch the current image catalog by checking missing and changed images
- Train a classifier on the image set
- Increment the classifier version and upload the new model
- Update the list of caps which can be recognized using the classifier
- Create missing thumbnails for each cap for the cap grid
A configuration file is required to run the training, with a valid access token for the server:
```json
{
"imageDirectory": "../Public/images",
"classifierModelPath": "../Public/classifier.mlmodel",
"trainingIterations": 20,
"serverPath": "https://mydomain.com/caps",
"authenticationToken": "mysecretkey",
}
```
The configuration file `config.json` must be located in the folder from which the script is run.
```bash
swift train.swift
```
## Future work ## Future work
- Create thumbnails on the server using [JPEG](https://github.com/kelvin13/jpeg) - Create thumbnails on the server using [JPEG](https://github.com/kelvin13/jpeg)

View File

@@ -3,6 +3,7 @@
"maxBodySize" : "2mb", "maxBodySize" : "2mb",
"logPath": "\/var\/log\/caps/metrics", "logPath": "\/var\/log\/caps/metrics",
"serveFiles": true, "serveFiles": true,
"dataDirectory" : "/ch/data/caps",
"writers" : [ "writers" : [
"auth_key_1" "auth_key_1"
] ]

View File

@@ -20,6 +20,20 @@ struct Config: Codable {
/// Authentication tokens for remotes allowed to write /// Authentication tokens for remotes allowed to write
let writers: [String] let writers: [String]
/**
The folder where the data should be stored.
If the folder is set to `nil`, then the `Resources` folder is used.
*/
let dataDirectory: String?
func customDataDirectory(or publicDirectory: String) -> URL {
guard let dataDirectory else {
return URL(fileURLWithPath: publicDirectory)
}
return URL(fileURLWithPath: dataDirectory)
}
func logURL(possiblyRelativeTo resourcesDirectory: URL) -> URL { func logURL(possiblyRelativeTo resourcesDirectory: URL) -> URL {
guard let logPath else { guard let logPath else {
return resourcesDirectory.appendingPathComponent("logs") return resourcesDirectory.appendingPathComponent("logs")

View File

@@ -3,32 +3,12 @@ import Clairvoyant
import Vapor import Vapor
import NIOCore import NIOCore
final class EventLoopScheduler { extension MultiThreadedEventLoopGroup: AsyncScheduler {
private let backgroundGroup: EventLoopGroup func test() {
init(numberOfThreads: Int = 2) {
backgroundGroup = MultiThreadedEventLoopGroup(numberOfThreads: numberOfThreads)
} }
func next() -> EventLoop { public func schedule(asyncJob: @escaping @Sendable () async throws -> Void) {
backgroundGroup.next() _ = any().makeFutureWithTask(asyncJob)
}
func provider() -> NIOEventLoopGroupProvider {
return .shared(backgroundGroup)
}
func shutdown() {
backgroundGroup.shutdownGracefully { _ in
}
}
}
extension EventLoopScheduler: AsyncScheduler {
func schedule(asyncJob: @escaping @Sendable () async throws -> Void) {
_ = backgroundGroup.any().makeFutureWithTask(asyncJob)
} }
} }

View File

@@ -7,17 +7,11 @@ import ClairvoyantBinaryCodable
private var provider: VaporMetricProvider! private var provider: VaporMetricProvider!
private var serverStatus: Metric<ServerStatus>! private var serverStatus: Metric<ServerStatus>!
private let asyncScheduler = EventLoopScheduler() private let asyncScheduler = MultiThreadedEventLoopGroup(numberOfThreads: 2)
private var server: CapServer! private var server: CapServer!
private func status(_ newStatus: ServerStatus) { func configure(_ app: Application) async throws {
asyncScheduler.schedule {
try await serverStatus.update(newStatus)
}
}
public func configure(_ app: Application) throws {
let resourceDirectory = URL(fileURLWithPath: app.directory.resourcesDirectory) let resourceDirectory = URL(fileURLWithPath: app.directory.resourcesDirectory)
let publicDirectory = app.directory.publicDirectory let publicDirectory = app.directory.publicDirectory
@@ -32,12 +26,13 @@ public func configure(_ app: Application) throws {
serverStatus = Metric<ServerStatus>("caps.status", serverStatus = Metric<ServerStatus>("caps.status",
name: "Status", name: "Status",
description: "The general status of the service") description: "The general status of the service")
status(.initializing) try await serverStatus.update(.initializing)
app.http.server.configuration.port = config.port app.http.server.configuration.port = config.port
app.routes.defaultMaxBodySize = .init(stringLiteral: config.maxBodySize) app.routes.defaultMaxBodySize = .init(stringLiteral: config.maxBodySize)
server = CapServer(in: URL(fileURLWithPath: publicDirectory)) let dataDirectory = config.customDataDirectory(or: publicDirectory)
server = CapServer(in: dataDirectory)
provider = .init(observer: monitor, accessManager: config.writers) provider = .init(observer: monitor, accessManager: config.writers)
provider.asyncScheduler = asyncScheduler provider.asyncScheduler = asyncScheduler
@@ -55,16 +50,27 @@ public func configure(_ app: Application) throws {
do { do {
try server.loadData() try server.loadData()
} catch { } catch {
status(.initializationFailure) try await serverStatus.update(.initializationFailure)
print("[\(df.string(from: Date()))] Server failed to start: \(error)") print("[\(df.string(from: Date()))] Server failed to start: \(error)")
return return
} }
if server.canResizeImages { if server.canResizeImages {
status(.nominal) try await serverStatus.update(.nominal)
} else { } else {
status(.reducedFunctionality) try await serverStatus.update(.reducedFunctionality)
}
print("[\(df.string(from: Date()))] Server started (\(app.environment.name), \(server.capCount) caps)")
}
func shutdown() {
Task {
print("[\(df.string(from: Date()))] Server shutdown")
do {
try await asyncScheduler.shutdownGracefully()
} catch {
print("Failed to shut down MultiThreadedEventLoopGroup: \(error)")
}
} }
print("[\(df.string(from: Date()))] Server started (\(server.capCount) caps)")
} }
func log(_ message: String) { func log(_ message: String) {

View File

@@ -0,0 +1,43 @@
import Vapor
import Dispatch
import Logging
/// This extension is temporary and can be removed once Vapor gets this support.
private extension Vapor.Application {
static let baseExecutionQueue = DispatchQueue(label: "vapor.codes.entrypoint")
func runFromAsyncMainEntrypoint() async throws {
try await withCheckedThrowingContinuation { continuation in
Vapor.Application.baseExecutionQueue.async { [self] in
do {
try self.run()
continuation.resume()
} catch {
continuation.resume(throwing: error)
}
}
}
}
}
@main
enum Entrypoint {
static func main() async throws {
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
defer {
shutdown()
app.shutdown()
}
do {
try await configure(app)
} catch {
app.logger.report(error: error)
throw error
}
try await app.runFromAsyncMainEntrypoint()
}
}

View File

@@ -1,9 +0,0 @@
import App
import Vapor
var env = Environment.production
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
defer { app.shutdown() }
try configure(app)
try app.run()

View File

View File

@@ -1,13 +0,0 @@
import App
import Dispatch
import XCTest
final class AppTests : XCTestCase {
func testNothing() throws {
XCTAssert(true)
}
static let allTests = [
("testNothing", testNothing),
]
}

View File