rc-1
This commit is contained in:
@@ -0,0 +1,751 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gifuu/tools"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
VIDEO_MAX_WIDTH = 3840
|
||||
VIDEO_MAX_HEIGHT = 2160
|
||||
IMAGE_MAX_WIDTH = 7680
|
||||
IMAGE_MAX_HEIGHT = 4320
|
||||
MEDIA_MIN_WIDTH = 64
|
||||
MEDIA_MIN_HEIGHT = 64
|
||||
MEDIA_MAX_DURATION = 62
|
||||
MEDIA_COLOR_SPACE = "yuv420p"
|
||||
MEDIA_COLOR_RANGE = "tv"
|
||||
MEDIA_FILENAME_PREVIEW = "preview.avif"
|
||||
MEDIA_FILENAME_STANDARD = "standard.avif"
|
||||
MEDIA_FILENAME_ALPHA = "alpha.webm"
|
||||
MEDIA_FILENAME_AUDIO = "standard.ogg"
|
||||
MEDIA_BACKGROUND = "#ffffff"
|
||||
VIDEO_ENCODE_CODEC = "libsvtav1"
|
||||
VIDEO_ENCODE_EFFORT = "7"
|
||||
VIDEO_ENCODE_PARAMS = "lp=2"
|
||||
VIDEO_FILTERS = ""
|
||||
VIDEO_KEYFRAME_INTERVAL = 6
|
||||
VIDEO_PREVIEW_MAX_DURATION = 8
|
||||
VIDEO_PREVIEW_FPS = 16
|
||||
VIDEO_PREVIEW_SIZE = 240
|
||||
VIDEO_PREVIEW_QUALITY = "55"
|
||||
VIDEO_STANDARD_FPS = 60
|
||||
VIDEO_STANDARD_SIZE = 720
|
||||
VIDEO_STANDARD_QUALITY = "50"
|
||||
IMAGE_ENCODE_CODEC = "libsvtav1"
|
||||
IMAGE_ENCODE_EFFORT = "7"
|
||||
IMAGE_ENCODE_QUALITY = "45"
|
||||
IMAGE_ENCODE_PARAMS = "lp=2"
|
||||
IMAGE_FILTERS = "tpad=stop_mode=clone:stop_duration=1,"
|
||||
IMAGE_LARGE_SIZE = 2160
|
||||
IMAGE_PREVIEW_SIZE = 240
|
||||
)
|
||||
|
||||
func ternary[T any](cond bool, a, b T) T {
|
||||
if cond {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func POST_Uploads(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// --- Update Request Lifetime ---
|
||||
sse, err := tools.NewEventHelper(ctx, w, r)
|
||||
if err != nil {
|
||||
tools.SendServerError(w, r, err)
|
||||
}
|
||||
|
||||
rc := http.NewResponseController(w)
|
||||
if err := rc.SetWriteDeadline(time.Now().Add(10 * time.Minute)); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Prevent Abuse ] ----------------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
var (
|
||||
ClientAddress = tools.RequestAddressHash(r)
|
||||
ClientToken = tools.RequestToken()
|
||||
ClientLength = max(0, r.ContentLength)
|
||||
)
|
||||
{
|
||||
// --- Request Upload Capacity ---
|
||||
if ClientLength == 0 {
|
||||
sse.SendClientError(tools.ERROR_BODY_EMPTY)
|
||||
return
|
||||
}
|
||||
if ClientLength > int64(tools.LIMIT_FILE+tools.LIMIT_JSON) {
|
||||
sse.SendClientError(tools.ERROR_BODY_TOO_LARGE)
|
||||
return
|
||||
}
|
||||
|
||||
if !tools.SEMA_UPLOADS.TryAcquire(ClientLength) {
|
||||
sse.SendClientError(tools.ERROR_SERVER_RESOURCES_EXHAUSTED)
|
||||
return
|
||||
}
|
||||
defer tools.SEMA_UPLOADS.Release(ClientLength)
|
||||
|
||||
// --- Check Ban List ---
|
||||
var SubjectCount int
|
||||
err := tools.Database.
|
||||
QueryRow(ctx, "SELECT COUNT(*) FROM gifuu.mod_banned WHERE address_hash = $1", ClientAddress).
|
||||
Scan(&SubjectCount)
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
if SubjectCount > 0 {
|
||||
sse.SendClientError(tools.ERROR_GENERIC_FORBIDDEN)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Parse Incoming Request ] -------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
var (
|
||||
TempID = tools.RequestSnowflake()
|
||||
TempIDString = strconv.FormatInt(TempID, 10)
|
||||
TempSuccess = false
|
||||
TempUpload *os.File
|
||||
TempLogger *os.File
|
||||
Body struct {
|
||||
Title string `json:"title"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
PathUpload = filepath.Join(tools.STORAGE_DISK_TEMP, TempIDString+".bin")
|
||||
PathLogger = filepath.Join(tools.STORAGE_DISK_TEMP, TempIDString+".log")
|
||||
PathAlpha = filepath.Join(tools.STORAGE_DISK_TEMP, TempIDString+"_"+MEDIA_FILENAME_ALPHA)
|
||||
PathPreview = filepath.Join(tools.STORAGE_DISK_TEMP, TempIDString+"_"+MEDIA_FILENAME_PREVIEW)
|
||||
PathStandard = filepath.Join(tools.STORAGE_DISK_TEMP, TempIDString+"_"+MEDIA_FILENAME_STANDARD)
|
||||
PathAudio = filepath.Join(tools.STORAGE_DISK_TEMP, TempIDString+"_"+MEDIA_FILENAME_AUDIO)
|
||||
)
|
||||
{
|
||||
// --- Create Temporary Files ---
|
||||
flags := os.O_CREATE | os.O_TRUNC | os.O_WRONLY
|
||||
|
||||
if f, err := os.OpenFile(PathUpload, flags, tools.FILE_PUBLIC); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
} else {
|
||||
TempUpload = f
|
||||
defer os.Remove(PathUpload)
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
if f, err := os.OpenFile(PathLogger, flags, tools.FILE_PRIVATE); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
} else {
|
||||
TempLogger = f
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
defer os.Remove(PathAlpha)
|
||||
defer os.Remove(PathPreview)
|
||||
defer os.Remove(PathStandard)
|
||||
defer os.Remove(PathAudio)
|
||||
|
||||
// --- Parse Form Body ---
|
||||
var haveFile, haveData bool
|
||||
reader, err := r.MultipartReader()
|
||||
if err != nil {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_DATA)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
part, err := reader.NextPart()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// --- Parse Incoming Metadata ---
|
||||
if part.FormName() == "data" {
|
||||
if haveData {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
haveData = true
|
||||
|
||||
if err := tools.ParseJSON(part, &Body); err != nil {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
break
|
||||
}
|
||||
|
||||
// Validate Struct
|
||||
if normal, ok := tools.NormalizeTitle(Body.Title); !ok {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
} else {
|
||||
Body.Title = normal
|
||||
}
|
||||
|
||||
indexTags := make(map[string]struct{}, len(Body.Tags))
|
||||
for i, given := range Body.Tags {
|
||||
normal, ok := tools.NormalizeTag(given)
|
||||
if _, exists := indexTags[normal]; exists || !ok {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
indexTags[normal] = struct{}{}
|
||||
Body.Tags[i] = normal
|
||||
}
|
||||
|
||||
fmt.Fprintf(TempLogger, "Collected JSON : %s\n", Body)
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Store Incoming Upload ---
|
||||
if part.FormName() == "file" {
|
||||
if haveFile {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
haveFile = true
|
||||
|
||||
// Check File Extension
|
||||
mediaAccept := false
|
||||
mediaType := part.Header.Get("Content-Type")
|
||||
for _, t := range tools.LIMIT_MIME_TYPE {
|
||||
if strings.EqualFold(t, mediaType) {
|
||||
mediaAccept = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !mediaAccept {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_CONTENT_TYPE)
|
||||
return
|
||||
}
|
||||
|
||||
// Copy File
|
||||
mediaSize, err := io.Copy(TempUpload, io.LimitReader(part, ClientLength))
|
||||
if err != nil {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_DATA)
|
||||
return
|
||||
}
|
||||
TempUpload.Close()
|
||||
|
||||
fmt.Fprintf(TempLogger,
|
||||
"Collected File : %s (Type: %s) (Size: %db)\n",
|
||||
mediaType, part.FileName(), mediaSize,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
|
||||
if !haveFile || !haveData {
|
||||
sse.SendClientError(tools.ERROR_BODY_INVALID_FIELD)
|
||||
return
|
||||
}
|
||||
|
||||
sse.SendJSON("id", TempIDString)
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Media Validation ] -------------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
var (
|
||||
ProbeResults tools.ProbeResults // Probe Results
|
||||
ProbeVideo *tools.ProbeStream // Relevant Video Stream
|
||||
ProbeAudio *tools.ProbeStream // Relevant Audio Stream
|
||||
MediaSticker bool // Is Static?
|
||||
MediaFramerate int // Approximate Framerate
|
||||
MediaHeight int // Approximate Scaled Height
|
||||
MediaWidth int // Approximate Scaled Width
|
||||
MediaRating float32 // Worst value from Classification
|
||||
)
|
||||
{
|
||||
sse.SendJSON("step", map[string]any{"id": "PROBE_QUEUE", "message": "Queued for Probing"})
|
||||
t := time.Now()
|
||||
|
||||
// --- Acquire Probe Slot ---
|
||||
if err := tools.SEMA_PROBES.Acquire(ctx, 1); err != nil {
|
||||
return
|
||||
}
|
||||
defer tools.SEMA_PROBES.Release(1)
|
||||
|
||||
// --- Probe Media Stream ---
|
||||
sse.SendJSON("step", map[string]any{"id": "PROBE_START", "message": "Probing"})
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.CommandContext(ctx, "ffprobe",
|
||||
"-hide_banner",
|
||||
"-loglevel", "verbose",
|
||||
"-print_format", "json",
|
||||
"-show_streams",
|
||||
"-i", PathUpload,
|
||||
)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
|
||||
fmt.Fprintf(TempLogger, "\n%s\n%s\nProbing completed in %s\n\n",
|
||||
// extra newline for proper log padding
|
||||
stdout.String(),
|
||||
stderr.String(),
|
||||
time.Since(t),
|
||||
)
|
||||
|
||||
if ctx.Err() == context.Canceled {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &ProbeResults); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Find Media Stream ---
|
||||
// isVideo: Only video streams (AV1, MP4, etc.)
|
||||
// isImage: Only select image streams if it's the only stream available
|
||||
for _, s := range ProbeResults.Streams {
|
||||
isVideo := (s.CodecType == "video")
|
||||
isImage := (s.CodecType == "image" && len(ProbeResults.Streams) == 1)
|
||||
isValid := true &&
|
||||
s.Width >= MEDIA_MIN_WIDTH && s.Height >= MEDIA_MIN_HEIGHT &&
|
||||
float64(s.Duration) <= float64(MEDIA_MAX_DURATION) &&
|
||||
ternary(s.NumberFrames < 2,
|
||||
s.Width < IMAGE_MAX_WIDTH && s.Height < IMAGE_MAX_HEIGHT,
|
||||
s.Width < VIDEO_MAX_WIDTH && s.Height < VIDEO_MAX_HEIGHT,
|
||||
)
|
||||
if isValid && (isVideo || isImage) && (ProbeVideo == nil || s.Duration > ProbeVideo.Duration) {
|
||||
ProbeVideo = &s
|
||||
}
|
||||
|
||||
isAudio := (s.CodecType == "audio")
|
||||
if isAudio {
|
||||
ProbeAudio = &s
|
||||
}
|
||||
}
|
||||
if ProbeVideo == nil {
|
||||
sse.SendClientError(tools.ERROR_MEDIA_INVALID)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Detect Properties ---
|
||||
|
||||
// Remove Audio from Stickers
|
||||
MediaSticker = (ProbeVideo.NumberFrames < 2)
|
||||
if MediaSticker {
|
||||
ProbeAudio = nil
|
||||
}
|
||||
|
||||
// Approximate Video Properties
|
||||
d := ternary(MediaSticker, IMAGE_LARGE_SIZE, VIDEO_STANDARD_SIZE)
|
||||
s := float64(min(ProbeVideo.Height, d)) / float64(ProbeVideo.Height)
|
||||
MediaWidth = int(float64(ProbeVideo.Width)*s) &^ 1
|
||||
MediaHeight = int(float64(ProbeVideo.Height)*s) &^ 1
|
||||
MediaFramerate = ternary(MediaSticker, 1, min(int(ProbeVideo.RFrameRate), VIDEO_STANDARD_FPS))
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Media Processing ] -------------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
{
|
||||
sse.SendJSON("step", map[string]any{"id": "ENCODE_QUEUE", "message": "Queued for Processing"})
|
||||
t := time.Now()
|
||||
|
||||
// --- Acquire Upload Slot ---
|
||||
if err := tools.SEMA_ENCODES.Acquire(ctx, 1); err != nil {
|
||||
return
|
||||
}
|
||||
defer tools.SEMA_ENCODES.Release(1)
|
||||
|
||||
// --- Prepare For Encoding ---
|
||||
sse.SendJSON("step", map[string]any{"id": "ENCODE_START", "message": "Processing"})
|
||||
|
||||
args := []string{
|
||||
"-loglevel", "verbose",
|
||||
"-hide_banner",
|
||||
"-stats",
|
||||
|
||||
"-i", PathUpload,
|
||||
"-filter_complex", fmt.Sprintf(""+
|
||||
"[0:%d]format=rgba[fg];[fg]split[fg1][fg2];[fg2]drawbox=x=0:y=0:w=iw:h=ih:color=%s:t=fill[bg];[bg][fg1]overlay[base];"+
|
||||
"[base]split=4[v1][v2][v3][v4];"+
|
||||
"[v1]%sscale=-2:%d:flags=lanczos,fps=%d[v1o];"+
|
||||
"[v2]%sscale=-2:%d:flags=lanczos,fps=%d[v2o];"+
|
||||
"[v3]%sscale=-2:%d:flags=lanczos,fps=%d[v3a];[v3a]format=rgba[v3f];[v3f]split[v3color][v3mask];[v3mask]alphaextract[v3ae];[v3color][v3ae]vstack[v3o];"+
|
||||
"[v4]%sscale=%d:%d:flags=neighbor,fps=%d[v4o];",
|
||||
|
||||
// Import
|
||||
ProbeVideo.Index,
|
||||
MEDIA_BACKGROUND,
|
||||
|
||||
// Export: Preview
|
||||
ternary(MediaSticker, IMAGE_FILTERS, VIDEO_FILTERS),
|
||||
min(ProbeVideo.Height, ternary(MediaSticker, IMAGE_PREVIEW_SIZE, VIDEO_PREVIEW_SIZE)),
|
||||
min(int(ProbeVideo.RFrameRate), ternary(MediaSticker, 1, VIDEO_PREVIEW_FPS)),
|
||||
|
||||
// Export: Standard
|
||||
ternary(MediaSticker, IMAGE_FILTERS, VIDEO_FILTERS),
|
||||
min(ProbeVideo.Height, ternary(MediaSticker, IMAGE_LARGE_SIZE, VIDEO_STANDARD_SIZE)),
|
||||
min(int(ProbeVideo.RFrameRate), ternary(MediaSticker, 1, VIDEO_STANDARD_FPS)),
|
||||
|
||||
// Export: Alpha
|
||||
ternary(MediaSticker, IMAGE_FILTERS, VIDEO_FILTERS),
|
||||
min(ProbeVideo.Height, ternary(MediaSticker, IMAGE_LARGE_SIZE, VIDEO_STANDARD_SIZE)),
|
||||
min(int(ProbeVideo.RFrameRate), ternary(MediaSticker, 1, VIDEO_STANDARD_FPS)),
|
||||
|
||||
// Export: Inference
|
||||
IMAGE_FILTERS,
|
||||
tools.MODEL_SIZE,
|
||||
tools.MODEL_SIZE,
|
||||
tools.MODEL_FRAMERATE,
|
||||
),
|
||||
|
||||
// Export Preview
|
||||
"-map", "[v1o]", "-an", "-sn",
|
||||
"-c:v" /*----------*/, ternary(MediaSticker, IMAGE_ENCODE_CODEC, VIDEO_ENCODE_CODEC),
|
||||
"-preset" /*-------*/, ternary(MediaSticker, IMAGE_ENCODE_EFFORT, VIDEO_ENCODE_EFFORT),
|
||||
"-qp" /*-----------*/, ternary(MediaSticker, IMAGE_ENCODE_QUALITY, VIDEO_PREVIEW_QUALITY),
|
||||
"-g" /*------------*/, strconv.Itoa(VIDEO_PREVIEW_FPS * VIDEO_KEYFRAME_INTERVAL),
|
||||
"-pix_fmt" /*------*/, MEDIA_COLOR_SPACE,
|
||||
"-color_range" /*--*/, MEDIA_COLOR_RANGE,
|
||||
"-svtav1-params" /**/, ternary(MediaSticker, IMAGE_ENCODE_PARAMS, VIDEO_ENCODE_PARAMS),
|
||||
"-frames:v" /*-----*/, ternary(MediaSticker, "1", strconv.Itoa(VIDEO_PREVIEW_FPS*VIDEO_PREVIEW_MAX_DURATION)),
|
||||
"-loop" /*---------*/, ternary(MediaSticker, "1", "0"),
|
||||
"-map_metadata" /*-*/, "-1",
|
||||
"-metadata", ("gifuu_machine=" + tools.MACHINE_HOSTNAME),
|
||||
"-metadata", ("gifuu_proverb=" + tools.MACHINE_PROVERB),
|
||||
PathPreview,
|
||||
|
||||
// Export: Standard
|
||||
"-map", "[v2o]", "-an", "-sn",
|
||||
"-c:v" /*----------*/, ternary(MediaSticker, IMAGE_ENCODE_CODEC, VIDEO_ENCODE_CODEC),
|
||||
"-preset" /*-------*/, ternary(MediaSticker, IMAGE_ENCODE_EFFORT, VIDEO_ENCODE_EFFORT),
|
||||
"-qp" /*-----------*/, ternary(MediaSticker, IMAGE_ENCODE_QUALITY, VIDEO_STANDARD_QUALITY),
|
||||
"-g" /*------------*/, strconv.Itoa(VIDEO_STANDARD_FPS * VIDEO_KEYFRAME_INTERVAL),
|
||||
"-pix_fmt" /*------*/, MEDIA_COLOR_SPACE,
|
||||
"-color_range" /*--*/, MEDIA_COLOR_RANGE,
|
||||
"-svtav1-params" /**/, ternary(MediaSticker, IMAGE_ENCODE_PARAMS, VIDEO_ENCODE_PARAMS),
|
||||
"-frames:v" /*-----*/, ternary(MediaSticker, "1", strconv.Itoa(VIDEO_STANDARD_FPS*MEDIA_MAX_DURATION)),
|
||||
"-loop" /*---------*/, ternary(MediaSticker, "1", "0"),
|
||||
"-map_metadata" /*-*/, "-1",
|
||||
"-metadata", ("gifuu_machine=" + tools.MACHINE_HOSTNAME),
|
||||
"-metadata", ("gifuu_proverb=" + tools.MACHINE_PROVERB),
|
||||
PathStandard,
|
||||
|
||||
// Export: Alpha
|
||||
"-map", "[v3o]", "-an", "-sn",
|
||||
"-c:v" /*----------*/, ternary(MediaSticker, IMAGE_ENCODE_CODEC, VIDEO_ENCODE_CODEC),
|
||||
"-preset" /*-------*/, ternary(MediaSticker, IMAGE_ENCODE_EFFORT, VIDEO_ENCODE_EFFORT),
|
||||
"-qp" /*-----------*/, ternary(MediaSticker, IMAGE_ENCODE_QUALITY, VIDEO_STANDARD_QUALITY),
|
||||
"-g" /*------------*/, strconv.Itoa(VIDEO_STANDARD_FPS * VIDEO_KEYFRAME_INTERVAL),
|
||||
"-pix_fmt" /*------*/, MEDIA_COLOR_SPACE,
|
||||
"-color_range" /*--*/, MEDIA_COLOR_RANGE,
|
||||
"-svtav1-params" /**/, IMAGE_ENCODE_PARAMS,
|
||||
"-frames:v" /*-----*/, ternary(MediaSticker, "1", strconv.Itoa(VIDEO_STANDARD_FPS*MEDIA_MAX_DURATION)),
|
||||
"-loop" /*---------*/, ternary(MediaSticker, "1", "0"),
|
||||
"-map_metadata" /*-*/, "-1",
|
||||
"-metadata", ("gifuu_machine=" + tools.MACHINE_HOSTNAME),
|
||||
"-metadata", ("gifuu_proverb=" + tools.MACHINE_PROVERB),
|
||||
PathAlpha,
|
||||
|
||||
// Export: Moderation
|
||||
"-map", "[v4o]", "-an", "-sn",
|
||||
"-f" /*-------*/, "rawvideo",
|
||||
"-fps_mode" /**/, "vfr",
|
||||
"-pix_fmt" /*-*/, "rgb24",
|
||||
"-",
|
||||
}
|
||||
|
||||
if ProbeAudio != nil {
|
||||
args = append(args,
|
||||
"-map", fmt.Sprintf("0:%d", ProbeAudio.Index), "-vn", "-sn",
|
||||
"-c:a", "libopus",
|
||||
"-b:a", "64k",
|
||||
"-af", "loudnorm=I=-16:TP=-2:LRA=11",
|
||||
"-ar", "48000",
|
||||
"-ac", "2",
|
||||
"-map_metadata", "-1",
|
||||
"-metadata", ("gifuu_machine=" + tools.MACHINE_HOSTNAME),
|
||||
"-metadata", ("gifuu_proverb=" + tools.MACHINE_PROVERB),
|
||||
PathAudio,
|
||||
)
|
||||
}
|
||||
|
||||
var encodeCtx, encodeCancel = context.WithCancel(ctx)
|
||||
defer encodeCancel()
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.CommandContext(encodeCtx, "ffmpeg", args...)
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Auto Moderation ---
|
||||
var (
|
||||
classifyError error
|
||||
classifyPercent float32
|
||||
classifyAllowed = true
|
||||
classifyComplete = make(chan struct{}, 1)
|
||||
frameSize = (tools.MODEL_SIZE * tools.MODEL_SIZE * 3)
|
||||
tensorSize = (tools.MODEL_FRAMERATE * frameSize)
|
||||
tensorData = make([]float32, tensorSize)
|
||||
frameData = make([]byte, tensorSize)
|
||||
)
|
||||
|
||||
go func() {
|
||||
defer close(classifyComplete)
|
||||
defer io.Copy(io.Discard, stdout)
|
||||
frameIndex := 0
|
||||
for {
|
||||
// Process Raw Frames for Model
|
||||
n, err := io.ReadFull(stdout, frameData)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil && err != io.ErrUnexpectedEOF {
|
||||
encodeCancel()
|
||||
return
|
||||
}
|
||||
|
||||
frameCount := n / frameSize
|
||||
for i := 0; i < n; i++ {
|
||||
tensorData[i] = float32(frameData[i]) / 255.0
|
||||
}
|
||||
|
||||
// Classify Frames
|
||||
logits, err := tools.ModelClassifyTensorBatch(
|
||||
tensorData[:frameCount*frameSize],
|
||||
frameCount,
|
||||
)
|
||||
if err != nil {
|
||||
classifyError = err
|
||||
encodeCancel()
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate Results
|
||||
for idx, results := range logits {
|
||||
classifyPercent = (results.Hentai + results.Porn + (results.Sexy * 0.9))
|
||||
classifyAllowed = (classifyPercent < tools.MODEL_THRESHOLD_DENY)
|
||||
|
||||
fmt.Fprintf(TempLogger,
|
||||
"#%02d | D: %.2f | H: %.2f | N: %.2f | P: %.2f | S: %.2f | T: %.2f%% | OK: %t\n",
|
||||
frameIndex+idx,
|
||||
results.Drawing,
|
||||
results.Hentai,
|
||||
results.Neutral,
|
||||
results.Porn,
|
||||
results.Sexy,
|
||||
classifyPercent,
|
||||
classifyAllowed,
|
||||
)
|
||||
|
||||
if classifyPercent > MediaRating {
|
||||
MediaRating = classifyPercent
|
||||
}
|
||||
|
||||
if !classifyAllowed {
|
||||
encodeCancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
frameIndex += frameCount
|
||||
|
||||
// Calculate Progress (Approximate)
|
||||
sse.SendJSON("progress", map[string]any{
|
||||
"percent": strconv.FormatFloat(
|
||||
min(100, (float64(frameIndex)/float64(tools.MODEL_FRAMERATE))/float64(ProbeVideo.Duration)*100),
|
||||
'f', 2, 64,
|
||||
),
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
// --- Wait for Results ---
|
||||
err = cmd.Wait()
|
||||
<-classifyComplete
|
||||
|
||||
fmt.Fprintf(TempLogger, "\n%s\nProcessing completed in %s\n",
|
||||
stderr.String(),
|
||||
time.Since(t),
|
||||
)
|
||||
|
||||
if ctx.Err() == context.Canceled {
|
||||
return
|
||||
}
|
||||
if !classifyAllowed {
|
||||
sse.SendClientError(tools.ERROR_MEDIA_INAPPROPRIATE)
|
||||
return
|
||||
}
|
||||
if classifyError != nil {
|
||||
sse.SendServerError(classifyError)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sse.SendJSON("step", map[string]any{"id": "SERVER_FINALIZE", "message": "Syncing"})
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Upload Objects ] ---------------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
{
|
||||
var (
|
||||
TargetDir = filepath.Join(tools.STORAGE_DISK_PUBLIC, TempIDString)
|
||||
TargetAlpha = filepath.Join(TargetDir, MEDIA_FILENAME_ALPHA)
|
||||
TargetPreview = filepath.Join(TargetDir, MEDIA_FILENAME_PREVIEW)
|
||||
TargetStandard = filepath.Join(TargetDir, MEDIA_FILENAME_STANDARD)
|
||||
TargetAudio = filepath.Join(TargetDir, MEDIA_FILENAME_AUDIO)
|
||||
)
|
||||
|
||||
// --- Create Directory ---
|
||||
if err := os.MkdirAll(TargetDir, tools.FILE_PUBLIC); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if !TempSuccess {
|
||||
if err := os.RemoveAll(TargetDir); err != nil {
|
||||
tools.LoggerStorage.Log(tools.WARN, "Failed to delete incomplete files: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// --- Move Files ---
|
||||
for _, op := range []struct {
|
||||
ShouldCopy bool
|
||||
PathSource string
|
||||
PathTarget string
|
||||
ContentType string
|
||||
}{
|
||||
{true, PathAlpha, TargetAlpha, "video/webm"},
|
||||
{true, PathPreview, TargetPreview, "image/avif"},
|
||||
{true, PathStandard, TargetStandard, "image/avif"},
|
||||
{ProbeAudio != nil, PathAudio, TargetAudio, "audio/ogg"},
|
||||
} {
|
||||
if !op.ShouldCopy {
|
||||
continue
|
||||
}
|
||||
|
||||
s, err := os.Open(op.PathSource)
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
t, err := os.OpenFile(op.PathTarget, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, tools.FILE_PUBLIC)
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
defer t.Close()
|
||||
|
||||
if _, err := io.Copy(t, s); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Upload Metadata ] --------------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
{
|
||||
// --- Begin Transaction ---
|
||||
tx, err := tools.Database.Begin(ctx)
|
||||
if err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
// --- Insert Row ---
|
||||
if _, err = tx.Exec(ctx,
|
||||
`INSERT INTO gifuu.upload (
|
||||
id,
|
||||
upload_address_hash,
|
||||
upload_token_hash,
|
||||
flag_sticker,
|
||||
flag_audio,
|
||||
encode_fps,
|
||||
encode_width,
|
||||
encode_height,
|
||||
meta_rating,
|
||||
meta_title
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
TempID,
|
||||
ClientAddress,
|
||||
tools.RequestHash(ClientToken),
|
||||
MediaSticker,
|
||||
ProbeAudio != nil,
|
||||
MediaFramerate,
|
||||
MediaWidth,
|
||||
MediaHeight,
|
||||
MediaRating,
|
||||
Body.Title,
|
||||
); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Upsert Tags ---
|
||||
if _, err := tx.Exec(ctx,
|
||||
`WITH upserted_tags AS (
|
||||
INSERT INTO gifuu.tag (label)
|
||||
SELECT unnest($1::text[])
|
||||
ON CONFLICT (label) DO UPDATE SET label = EXCLUDED.label
|
||||
RETURNING id
|
||||
)
|
||||
INSERT INTO gifuu.upload_tag (gif_id, tag_id)
|
||||
SELECT $2, id FROM upserted_tags`,
|
||||
Body.Tags,
|
||||
TempID,
|
||||
); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Commit Transaction ---
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
sse.SendServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// --- [ Return Results ] ---------------------------------------------------------
|
||||
// --------------------------------------------------------------------------------
|
||||
TempSuccess = true
|
||||
sse.SendJSON("finish", map[string]any{
|
||||
"id": strconv.FormatInt(TempID, 10),
|
||||
"edit_token": ClientToken,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user