752 lines
22 KiB
Go
752 lines
22 KiB
Go
|
|
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,
|
||
|
|
})
|
||
|
|
}
|