# Shared model definition + feature extraction, used by BOTH the training script # (bin/train.jl) and the server (worker inference). Keeping the architecture and # the byte->feature mapping in one place guarantees train and inference can never # drift out of sync (same input layout, same scaling, same net shape). # # The including scope is expected to have already done `using Lux` and # `using JLD2` (FileServer.jl and bin/train.jl both do), so this file adds no # top-level `using` of its own and works whether it's pulled into the FileServer # module or a bare script. "Number of input features: first 16 bytes + last 16 bytes of a file." const FEATURE_DIM = 32 "Minimum file size (bytes) the model can accept: needs a disjoint 16+16 window." const MIN_FILE_BYTES = 32 """ build_model() The fixed-structure classifier: 32-dim byte features → two logits (class 1 = known, class 2 = unknown). Raw logits out (no softmax layer); the loss applies log-softmax and inference just takes an argmax. """ build_model() = Chain( Dense(FEATURE_DIM => 64, relu), Dense(64 => 16, relu), Dense(16 => 2), ) """ read_features(path) -> Union{Vector{Float32}, Nothing} Read the first 16 and last 16 bytes of the file at `path`, concatenate to a 32-element vector, and scale each byte from 0–255 into [0,1]. Returns `nothing` for files shorter than `MIN_FILE_BYTES` (the caller decides what that means: training drops them, the server classifies them as unknown without the model). Memory stays flat: we read 16 bytes from the front and `seek` to end-16 for the tail rather than slurping the whole file. """ function read_features(path::AbstractString) sz = filesize(path) sz < MIN_FILE_BYTES && return nothing bytes = open(path) do io head = read(io, 16) seek(io, sz - 16) tail = read(io, 16) vcat(head, tail) end return Float32.(bytes) ./ 255f0 end