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
Package.resolved
.swiftpm/
Public/caps.json
Public/changes.txt
Public/classifier.*
Public/count.*
Public/images/
Public/thumbnails/
Public/classifier.version
Public/classifier.mlmodel
Public/caps.json
Training/backup/
Training/config.json
Public/thumbnails
Public/count.js
Resources/config.json
Resources/logs/

View File

@@ -13,21 +13,15 @@ let package = Package(
.package(url: "https://github.com/christophhagen/ClairvoyantBinaryCodable", from: "0.3.0"),
],
targets: [
.target(name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Clairvoyant", package: "Clairvoyant"),
.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"]),
.executableTarget(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Clairvoyant", package: "Clairvoyant"),
.product(name: "ClairvoyantVapor", package: "ClairvoyantVapor"),
.product(name: "ClairvoyantBinaryCodable", package: "ClairvoyantBinaryCodable"),
]
)
]
)

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 name="robots" content="noindex"/>
<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="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png?v=1"/>
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png?v=1"/>
<link rel="manifest" href="/assets/icons/site.webmanifest?v=1"/>
<link rel="shortcut icon" href="/assets/icons/favicon.ico?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/favicon-32x32.png?v=1"/>
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png?v=1"/>
<link rel="manifest" href="/assets/site.webmanifest?v=1"/>
<link rel="shortcut icon" href="/assets/favicon.ico?v=1"/>
<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"/>
<link href="grid.css?v=2" rel="stylesheet"/>
<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
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.
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
```
This has to be done on macOS using the [Caps-Train](https://christophhagen.de/git/ch/Caps-Train) repository.
## Future work
- Create thumbnails on the server using [JPEG](https://github.com/kelvin13/jpeg)

View File

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

View File

@@ -20,6 +20,20 @@ struct Config: Codable {
/// Authentication tokens for remotes allowed to write
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 {
guard let logPath else {
return resourcesDirectory.appendingPathComponent("logs")

View File

@@ -3,32 +3,12 @@ import Clairvoyant
import Vapor
import NIOCore
final class EventLoopScheduler {
extension MultiThreadedEventLoopGroup: AsyncScheduler {
private let backgroundGroup: EventLoopGroup
init(numberOfThreads: Int = 2) {
backgroundGroup = MultiThreadedEventLoopGroup(numberOfThreads: numberOfThreads)
func test() {
}
func next() -> EventLoop {
backgroundGroup.next()
}
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)
public func schedule(asyncJob: @escaping @Sendable () async throws -> Void) {
_ = any().makeFutureWithTask(asyncJob)
}
}

View File

@@ -7,17 +7,11 @@ import ClairvoyantBinaryCodable
private var provider: VaporMetricProvider!
private var serverStatus: Metric<ServerStatus>!
private let asyncScheduler = EventLoopScheduler()
private let asyncScheduler = MultiThreadedEventLoopGroup(numberOfThreads: 2)
private var server: CapServer!
private func status(_ newStatus: ServerStatus) {
asyncScheduler.schedule {
try await serverStatus.update(newStatus)
}
}
public func configure(_ app: Application) throws {
func configure(_ app: Application) async throws {
let resourceDirectory = URL(fileURLWithPath: app.directory.resourcesDirectory)
let publicDirectory = app.directory.publicDirectory
@@ -32,12 +26,13 @@ public func configure(_ app: Application) throws {
serverStatus = Metric<ServerStatus>("caps.status",
name: "Status",
description: "The general status of the service")
status(.initializing)
try await serverStatus.update(.initializing)
app.http.server.configuration.port = config.port
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.asyncScheduler = asyncScheduler
@@ -55,16 +50,27 @@ public func configure(_ app: Application) throws {
do {
try server.loadData()
} catch {
status(.initializationFailure)
try await serverStatus.update(.initializationFailure)
print("[\(df.string(from: Date()))] Server failed to start: \(error)")
return
}
if server.canResizeImages {
status(.nominal)
try await serverStatus.update(.nominal)
} 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) {

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