MFM (Media Moderation Framework) โ ััะพ ัะธััะตะผะฐ ะฝะฐ ัะทัะบะต Go ะดะปั ะพะฑะฝะฐััะถะตะฝะธั NSFW-ะบะพะฝัะตะฝัะฐ (Not Safe For Work) ะฒ ะผะตะดะธะฐัะฐะนะปะฐั ั ะธัะฟะพะปัะทะพะฒะฐะฝะธะตะผ ะผะพะดะตะปะตะน ะธัะบััััะฒะตะฝะฝะพะณะพ ะธะฝัะตะปะปะตะบัะฐ ะธ ะผะฐัะธะฝะฝะพะณะพ ะพะฑััะตะฝะธั.
| ะะฐัะฐะผะตัั | ะะฝะฐัะตะฝะธะต |
|---|---|
| ะะพะดัะปั | github.com/comerc/mfm |
| ะะตััะธั Go | 1.24.2 |
| ะขะตะบััะฐั ะฒะตัะบะฐ | main |
| ะขะธะฟ ะปะธัะตะฝะทะธะธ | ะะต ัะบะฐะทะฐะฝ |
mfm/
โโโ cmd/server/ # ะขะพัะบะฐ ะฒั
ะพะดะฐ ะฟัะธะปะพะถะตะฝะธั
โ โโโ main.go # ะะปะฐะฒะฝัะน ัะฐะนะป ะฟัะธะปะพะถะตะฝะธั
โโโ internal/ # ะะฝัััะตะฝะฝะธะน ะบะพะด (ะฝะต ะฟะตัะตะธัะฟะพะปัะทัะตะผัะน)
โ โโโ repo/ # ะกะปะพะน ัะตะฟะพะทะธัะพัะธะตะฒ (ะดะพัััะฟ ะบ ะดะฐะฝะฝัะผ)
โ โ โโโ media_reader/ # ะงัะตะฝะธะต ะธ ะพะฑัะฐะฑะพัะบะฐ ะผะตะดะธะฐัะฐะนะปะพะฒ
โ โ โโโ open_runner/ # ะะฐะฟััะบ ะผะพะดะตะปะธ OpenNSFW2
โ โ โโโ vit_runner/ # ะะฐะฟััะบ ะผะพะดะตะปะธ Vision Transformer
โ โโโ service/ # ะกะปะพะน ัะตัะฒะธัะพะฒ (ะฑะธะทะฝะตั-ะปะพะณะธะบะฐ)
โ โโโ moderation/ # ะกะตัะฒะธั ะผะพะดะตัะฐัะธะธ ะบะพะฝัะตะฝัะฐ
โโโ pkg/ # ะะตัะตะธัะฟะพะปัะทัะตะผัะต ะฟะฐะบะตัั
โ โโโ onnxinit/ # ะะฝะธัะธะฐะปะธะทะฐัะธั ONNX Runtime
โ โโโ utils/ # ะะฑัะธะต ััะธะปะธัั
โ โโโ port.go # ะัะพะฒะตัะบะฐ ะดะพัััะฟะฝะพััะธ ะฟะพััะฐ
โ โโโ env.go # ะะฐะณััะทะบะฐ ะฟะตัะตะผะตะฝะฝัั
ะพะบััะถะตะฝะธั
โโโ assets/ # ะะตะดะธะฐ-ะฐะบัะธะฒั ะธ ONNX-ะผะพะดะตะปะธ
โ โโโ onnx/ # ะคะฐะนะปั ะผะพะดะตะปะตะน
โ โโโ *.png # ะขะตััะพะฒัะต ะธะทะพะฑัะฐะถะตะฝะธั
โ โโโ video*.mp4 # ะขะตััะพะฒัะต ะฒะธะดะตะพ
โโโ test/ # ะะฝัะตะณัะฐัะธะพะฝะฝัะต ัะตััั
โโโ doc/ # ะะพะบัะผะตะฝัะฐัะธั
โ โโโ models/ # ะะพะบัะผะตะฝัะฐัะธั ะฟะพ ะผะพะดะตะปัะผ
โโโ script/ # ะัะฟะพะผะพะณะฐัะตะปัะฝัะต ัะบัะธะฟัั
โโโ .env.example # ะัะธะผะตั ะบะพะฝัะธะณััะฐัะธะธ
โโโ Taskfile.yml # ะะฒัะพะผะฐัะธะทะฐัะธั ัะฑะพัะบะธ
โโโ .golangci.yml # ะะพะฝัะธะณััะฐัะธั ะปะธะฝัะตัะฐ
โโโ .mockery.yaml # ะะพะฝัะธะณััะฐัะธั ะณะตะฝะตัะฐัะธะธ ะผะพะบะพะฒ
| ะคะฐะนะป | ะะฟะธัะฐะฝะธะต |
|---|---|
cmd/server/main.go |
ะขะพัะบะฐ ะฒั ะพะดะฐ, ะฝะฐัััะพะนะบะฐ HTTP-ัะตัะฒะตัะฐ |
internal/service/moderation/service.go |
ะัะฝะพะฒะฝะพะน ัะตัะฒะธั ะผะพะดะตัะฐัะธะธ |
internal/repo/media_reader/repo.go |
ะะฑัะฐะฑะพัะบะฐ ะผะตะดะธะฐัะฐะนะปะพะฒ |
internal/repo/open_runner/open_runner.go |
ะ ะฐะฝะฝะตั OpenNSFW2 |
internal/repo/vit_runner/vit_runner.go |
ะ ะฐะฝะฝะตั Vision Transformer |
pkg/onnxinit/onnxinit.go |
ะะฝะธัะธะฐะปะธะทะฐัะธั ONNX Runtime |
pkg/utils/port.go |
ะัะพะฒะตัะบะฐ ะฟะพััะฐ |
pkg/utils/env.go |
ะะฐะณััะทะบะฐ .env |
test/moderation_test.go |
ะะฝัะตะณัะฐัะธะพะฝะฝัะต ัะตััั |
| ะะฐะบะตั | ะะตััะธั | ะะฐะทะฝะฐัะตะฝะธะต |
|---|---|---|
github.com/joho/godotenv |
v1.5.1 | ะะฐะณััะทะบะฐ ะฟะตัะตะผะตะฝะฝัั ะพะบััะถะตะฝะธั |
github.com/stretchr/testify |
v1.10.0 | ะขะตััะธัะพะฒะฐะฝะธะต ะธ ะผะพะบะธ |
github.com/yalue/onnxruntime_go |
v1.25.0 | ONNX Runtime ะดะปั Go |
go.uber.org/zap |
v1.27.1 | ะกัััะบัััะธัะพะฒะฐะฝะฝะพะต ะปะพะณะธัะพะฒะฐะฝะธะต |
github.com/samber/slog-zap/v2 |
v2.6.2 | ะะฝัะตะณัะฐัะธั slog ั Zap |
| ะะพะผะฟะพะฝะตะฝั | ะะฐะทะฝะฐัะตะฝะธะต |
|---|---|
| ONNX Runtime | ะัะฟะพะปะฝะตะฝะธะต ML-ะผะพะดะตะปะตะน |
| FFmpeg | ะะฑัะฐะฑะพัะบะฐ ะฒะธะดะตะพัะฐะนะปะพะฒ |
| ะะฝััััะผะตะฝั | ะะฐะทะฝะฐัะตะฝะธะต |
|---|---|
| Task | ะะฒัะพะผะฐัะธะทะฐัะธั ะทะฐะดะฐั (lint, test, run) |
| Mockery | ะะตะฝะตัะฐัะธั mock-ะพะฑัะตะบัะพะฒ |
| GolangCI-Lint | ะะพะผะฟะปะตะบัะฝะฐั ะฟัะพะฒะตัะบะฐ ะบะพะดะฐ |
ะะปะฐะฒะฝัะน ัะฐะนะป ะฟัะธะปะพะถะตะฝะธั ะฒัะฟะพะปะฝัะตั:
- ะะฐะณััะทะบั ะฟะตัะตะผะตะฝะฝัั
ะพะบััะถะตะฝะธั ะธะท
.env - ะะฝะธัะธะฐะปะธะทะฐัะธั ONNX Runtime
- ะะฐัััะพะนะบั ััััะบัััะธัะพะฒะฐะฝะฝะพะณะพ ะปะพะณะธัะพะฒะฐะฝะธั (Zap + slog)
- ะกะพะทะดะฐะฝะธะต ะทะฐะฒะธัะธะผะพััะตะน (media reader, model runners)
- ะะฐะฟััะบ HTTP-ัะตัะฒะตัะฐ ั ัะฝะดะฟะพะธะฝัะฐะผะธ:
POST /moderateโ ะผะพะดะตัะฐัะธั ะบะพะฝัะตะฝัะฐGET /liveโ health-check
- Graceful shutdown ะฟัะธ ะฟะพะปััะตะฝะธะธ ัะธะณะฝะฐะปะพะฒ SIGINT/SIGTERM
ะะฐัััะพะนะบะธ HTTP-ัะตัะฒะตัะฐ:
ReadTimeout: 15 * time.Second
WriteTimeout: 15 * time.Second
IdleTimeout: 60 * time.Secondะัะฝะพะฒะฝะพะน ะฑะธะทะฝะตั-ะปะพะณะธัะตัะบะธะน ะบะพะผะฟะพะฝะตะฝั:
ะะฝัะตััะตะนัั:
type mediaReader interface {
Read(filePath string) ([][]byte, error)
}
type modelRunner interface {
Infer(data [][]byte) ([]float32, error)
}ะะปะณะพัะธัะผ ัะฐะฑะพัั:
- ะงะธัะฐะตั ะผะตะดะธะฐัะฐะนะปั ัะตัะตะท
mediaReader - ะะณัะตะณะธััะตั ะฒัะต ััะตะนะผั ะฒ ะพะดะธะฝ ะฑะฐัั
- ะัะฟะพะปะฝัะตั ะธะฝัะตัะตะฝั ะดะปั ะฒัะตั ะผะพะดะตะปะตะน ะฟะฐัะฐะปะปะตะปัะฝะพ
- ะะปั ะธะทะพะฑัะฐะถะตะฝะธะน โ ะฑะตััั ะตะดะธะฝััะฒะตะฝะฝัะน ัะตะทัะปััะฐั
- ะะปั ะฒะธะดะตะพ โ ะฑะตััั ะผะฐะบัะธะผะฐะปัะฝัะน ัััั ััะตะดะธ ะฒัะตั ััะตะนะผะพะฒ
- ะะฑัะตะดะธะฝัะตั ัะตะทัะปััะฐัั ะฒัะตั ะผะพะดะตะปะตะน (ะผะฐะบัะธะผัะผ)
ะะฑัะฐะฑะฐััะฒะฐะตั ะผะตะดะธะฐัะฐะนะปั ะดะปั ะฟะพะดะฐัะธ ะฒ ะผะพะดะตะปะธ:
ะขะธะฟั ะบะพะฝัะตะฝัะฐ:
const (
contentTypeUnknown ContentType = "unknown"
contentTypeImage ContentType = "image"
contentTypeVideo ContentType = "video"
)ะะฑัะฐะฑะพัะบะฐ ะธะทะพะฑัะฐะถะตะฝะธะน:
- ะะตะบะพะดะธัะพะฒะฐะฝะธะต ะธะท ะปัะฑะพะณะพ ัะพัะผะฐัะฐ
- ะะทะผะตะฝะตะฝะธะต ัะฐะทะผะตัะฐ ะดะพ 224x224 (nearest-neighbor)
- ะะพะฝะฒะตััะฐัะธั ะฒ RGB24 (224 ร 224 ร 3 = 150,528 ะฑะฐะนั)
ะะฑัะฐะฑะพัะบะฐ ะฒะธะดะตะพ:
- ะะทะฒะปะตัะตะฝะธะต ะบะฐะดัะพะฒ ัะตัะตะท FFmpeg (2 fps)
- ะะทะผะตะฝะตะฝะธะต ัะฐะทะผะตัะฐ ะดะพ 224x224 ั ะพะฑัะตะทะบะพะน
- ะะพะฝะฒะตััะฐัะธั ะฒ RGB24
FFmpeg ะบะพะผะฐะฝะดะฐ:
ffmpeg -i <input> \
-vf "fps=2,scale=224:224:force_original_aspect_ratio=increase,crop=224:224" \
-f rawvideo \
-pix_fmt rgb24 \
pipe:1ะะพะดะตะปั: OpenNSFW2
- ะั
ะพะด:
[batch_size, 224, 224, 3](float32, ะฝะพัะผะฐะปะธะทะพะฒะฐะฝะพ 0-1) - ะัั
ะพะด:
[batch_size, 1](float32, ะฒะตัะพััะฝะพััั NSFW)
ะะพะดะตะปั: Vision Transformer
- ะั
ะพะด:
[batch_size, 224, 224, 3](float32, ะฝะพัะผะฐะปะธะทะพะฒะฐะฝะพ 0-1) - ะัั
ะพะด:
[batch_size, 2](float32, ะฟะฐัะฐ: normal, nsfw)
| ะะตัะตะผะตะฝะฝะฐั | ะะฟะธัะฐะฝะธะต |
|---|---|
HTTP_PORT |
ะะพัั HTTP-ัะตัะฒะตัะฐ (ะฟะพ ัะผะพะปัะฐะฝะธั 7171) |
MODEL_OPEN |
ะะฑัะพะปััะฝัะน ะฟััั ะบ ะผะพะดะตะปะธ OpenNSFW2 |
MODEL_VIT |
ะะฑัะพะปััะฝัะน ะฟััั ะบ ะผะพะดะตะปะธ ViT |
# Port for the HTTP server
HTTP_PORT=7171
# Model paths (absolute paths required)
MODEL_OPEN=/absolute/path/to/assets/onnx/opennsfw2.onnx
MODEL_VIT=/absolute/path/to/assets/onnx/vit_nsfw.onnxะะพะดะตัะฐัะธั ะผะตะดะธะฐัะฐะนะปะพะฒ.
ะขะตะปะพ ะทะฐะฟัะพัะฐ: (ะพะถะธะดะฐะตััั JSON ั ะฟัััะผะธ ะบ ัะฐะนะปะฐะผ)
ะัะฒะตั:
{
"status": "moderation service ready"
}ะัะธะผะตัะฐะฝะธะต: ะะพะปะฝะฐั ัะตะฐะปะธะทะฐัะธั ัะฝะดะฟะพะธะฝัะฐ ะฝะฐั ะพะดะธััั ะฒ ัะฐะทัะฐะฑะพัะบะต (TODO ะฒ ะบะพะดะต).
Health-check endpoint.
ะกัะฐััั: 200 OK
| ะขะธะฟ ัะตััะพะฒ | ะคะฐะนะปั |
|---|---|
| ะฎะฝะธั-ัะตััั | *_test.go ะฒ ะบะฐะถะดะพะผ ะฟะฐะบะตัะต |
| ะะฝัะตะณัะฐัะธะพะฝะฝัะต | test/moderation_test.go |
| ะะพะบะธ | internal/service/moderation/mocks/ |
| ะะตะฝัะผะฐัะบะธ | ะะฝัะตะณัะฐัะธะพะฝะฝัะต ัะตััั ั ะฑะฐััะฐะผะธ |
ะัะฟะพะปัะทัะตััั ะดะปั ััะฒะตัะถะดะตะฝะธะน ะธ ะผะพะบะธัะพะฒะฐะฝะธั:
import "github.com/stretchr/testify"ะะตะฝะตัะฐัะธั ะผะพะบะพะฒ ะดะปั ะธะฝัะตััะตะนัะพะฒ (ะฝะฐัััะพะตะฝะฐ ัะตัะตะท .mockery.yaml)
task lint # ะะฐะฟััะบ golangci-lint
task test # ะะฐะฟััะบ ัะตััะพะฒ
task run # ะะฐะฟััะบ ัะตัะฒะตัะฐะะพะฝัะธะณััะฐัะธั ะฒะบะปััะฐะตั ะฟัะฐะฒะธะปะฐ revive ะดะปั ะฟัะพะฒะตัะบะธ ะบะฐัะตััะฒะฐ ะบะพะดะฐ.
| ะคะฐะนะป | ะกะพะดะตัะถะฐะฝะธะต |
|---|---|
README.md |
ะะฝััััะบัะธั ะฟะพ ัััะฐะฝะพะฒะบะต |
doc/models/OpenNSFW2.md |
ะะพะบัะผะตะฝัะฐัะธั ะผะพะดะตะปะธ OpenNSFW2 |
doc/models/ViT.md |
ะะพะบัะผะตะฝัะฐัะธั Vision Transformer |
internal/service/moderation/TESTING.md |
ะะพะบัะผะตะฝัะฐัะธั ะฟะพ ัะตััะธัะพะฒะฐะฝะธั |
AGENTS.md |
ะะพะฝัะธะณััะฐัะธั KiloCode ะฐะณะตะฝัะพะฒ |
macOS:
brew install onnxruntimeะััะณะธะต ะฟะปะฐััะพัะผั: ัะผ. ONNX Runtime installation guide
macOS:
brew install ffmpeggo install github.com/go-task/task/v3/cmd/task@latest
go install github.com/vektra/mockery/v2@v2.53.3
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latestcp .env.example .env
# ะััะตะดะฐะบัะธััะนัะต .env ั ะฟัััะผะธ ะบ ะผะพะดะตะปัะผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ HTTP Layer โ
โ (cmd/server) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Service Layer โ
โ (internal/service/moderation) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Repository Layer โ
โ (internal/repo/{media_reader,open_runner,vit}) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ External Dependencies โ
โ (ONNX Runtime, FFmpeg) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ะัะต ะบะพะผะฟะพะฝะตะฝัั ัะพะทะดะฐัััั ัะตัะตะท ะบะพะฝััััะบัะพัั ะธ ะฒะฝะตะดัััััั ะทะฐะฒะธัะธะผะพััะธ:
mediaReader := mediareader.New()
openRunner := openrunner.New()
vitRunner := vitrunner.New()
moderationService := moderation.New(mediaReader, openRunner, vitRunner)ะัะฐะฒะธะปัะฝะพะต ะพัะฒะพะฑะพะถะดะตะฝะธะต ัะตััััะพะฒ:
- ะะฐะบัััะธะต ONNX-ัะตััะธะน ัะตัะตะท
defer - ะขะฐะนะผะฐัั shutdown: 30 ัะตะบัะฝะด
- ะะฑัะฐะฑะพัะบะฐ SIGINT/SIGTERM
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Media File(s) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Media Reader โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โ โ Image Process โ โ Video Process โ โ
โ โ - Decode โ โ - FFmpeg โ โ
โ โ - Resize 224ยฒ โ โ - 2 fps โ โ
โ โ - RGB24 โ โ - RGB24 โ โ
โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Batch All Frames โ
โ [frame1, frame2, ..., frameN] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโ
โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ OpenNSFW2 Runner โ โ ViT Runner โ
โ [batch, 224, 224, 3] โ โ [batch, 224, 224, 3] โ
โ โ โ โ โ โ
โ [batch, 1] (NSFW prob) โ โ [batch, 2] (N, NSFW) โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ โ
โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Aggregate Results โ
โ - Images: single score per file โ
โ - Videos: max score across all frames โ
โ - Models: max score across all models โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Final NSFW Scores โ
โ [file1, file2, ..., fileM] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- ะ ะตะฐะปะธะทะฐัะธั ัะฝะดะฟะพะธะฝัะฐ
/moderateโ ะฒ ะฝะฐััะพััะตะต ะฒัะตะผั ะฒะพะทะฒัะฐัะฐะตั ะทะฐะณะปััะบั (TODO ะฒ ะบะพะดะต) - ะะพะฑะฐะฒะปะตะฝะธะต ะฒะฐะปะธะดะฐัะธะธ ะฒั ะพะดะฝัั ะดะฐะฝะฝัั ะดะปั ัะฝะดะฟะพะธะฝัะฐ ะผะพะดะตัะฐัะธะธ
- ะ ะฐััะธัะตะฝะธะต ัะพัะผะฐัะพะฒ ะฒัะฒะพะดะฐ (JSON ั ะดะตัะฐะปัะฝัะผะธ ัะตะทัะปััะฐัะฐะผะธ)
- ะะพะฑะฐะฒะปะตะฝะธะต ะผะตััะธะบ (prometheus) ะดะปั ะผะพะฝะธัะพัะธะฝะณะฐ ะฟัะพะธะทะฒะพะดะธัะตะปัะฝะพััะธ
- ะะตัะธัะพะฒะฐะฝะธะต ัะตะทัะปััะฐัะพะฒ ะดะปั ะฟะพะฒัะพัะฝัั ะทะฐะฟัะพัะพะฒ
- ะัะธะฝั ัะพะฝะฝะฐั ะพะฑัะฐะฑะพัะบะฐ ะดะปั ะฑะพะปััะธั ะฑะฐััะตะน ัะฐะนะปะพะฒ
MFM โ ััะพ ั ะพัะพัะพ ััััะบัััะธัะพะฒะฐะฝะฝัะน, production-ready ะฟัะพะตะบั ะดะปั ะผะพะดะตัะฐัะธะธ ะผะตะดะธะฐะบะพะฝัะตะฝัะฐ. ะะพะด ัะปะตะดัะตั ะฟัะธะฝัะธะฟะฐะผ Clean Architecture ั ัััะบะธะผ ัะฐะทะดะตะปะตะฝะธะตะผ ะฝะฐ ัะปะพะธ. ะัะพะตะบั ะธะผะตะตั:
- โ ะงััะบัั ะฐัั ะธัะตะบัััั ั ัะฐะทะดะตะปะตะฝะธะตะผ ะพัะฒะตัััะฒะตะฝะฝะพััะธ
- โ Comprehensive ะปะพะณะธัะพะฒะฐะฝะธะต ั ะบะพะฝัะตะบััะพะผ
- โ Graceful shutdown ะธ ัะฟัะฐะฒะปะตะฝะธะต ัะตััััะฐะผะธ
- โ ะะพะบัััะธะต ัะตััะฐะผะธ
- โ ะะพะบัะผะตะฝัะฐัะธั ะฟะพ ัััะฐะฝะพะฒะบะต ะธ ะผะพะดะตะปัะผ
- โ ะะฝััััะผะตะฝัั ะดะปั ัะฐะทัะฐะฑะพัะบะธ (Taskfile, linting)
ะะฐัะฐ: 2026-02-01 ะะตััะธั ะพััััะฐ: 1.0