Add option to generate image thumbnails during generate (#4602)

* Add option to generate image thumbnails
* Limit number of concurrent image thumbnail generation ops
This commit is contained in:
WithoutPants 2024-02-22 11:19:23 +11:00 committed by GitHub
parent c4a91d15a6
commit a8c909e0c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 332 additions and 222 deletions

View File

@ -12,6 +12,7 @@ input GenerateMetadataInput {
forceTranscodes: Boolean
phashes: Boolean
interactiveHeatmapsSpeeds: Boolean
imageThumbnails: Boolean
clipPreviews: Boolean
"scene ids to generate for"
@ -48,6 +49,7 @@ type GenerateMetadataOptions {
transcodes: Boolean
phashes: Boolean
interactiveHeatmapsSpeeds: Boolean
imageThumbnails: Boolean
clipPreviews: Boolean
}

View File

@ -46,8 +46,9 @@ func (rs imageRoutes) Routes() chi.Router {
}
func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
mgr := manager.GetInstance()
img := r.Context().Value(imageKey).(*models.Image)
filepath := manager.GetInstance().Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
filepath := mgr.Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
// if the thumbnail doesn't exist, encode on the fly
exists, _ := fsutil.FileExists(filepath)
@ -62,6 +63,11 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
return
}
// use the image thumbnail generate wait group to limit the number of concurrent thumbnail generation tasks
wg := &mgr.ImageThumbnailGenerateWaitGroup
wg.Add()
defer wg.Done()
clipPreviewOptions := image.ClipPreviewOptions{
InputArgs: manager.GetInstance().Config.GetTranscodeInputArgs(),
OutputArgs: manager.GetInstance().Config.GetTranscodeOutputArgs(),

View File

@ -9,6 +9,7 @@ import (
"strings"
"time"
"github.com/remeh/sizedwaitgroup"
"github.com/stashapp/stash/internal/desktop"
"github.com/stashapp/stash/internal/dlna"
"github.com/stashapp/stash/internal/log"
@ -80,6 +81,8 @@ func Initialize(cfg *config.Config, l *log.Logger) (*Manager, error) {
Paths: mgrPaths,
ImageThumbnailGenerateWaitGroup: sizedwaitgroup.New(1),
JobManager: initJobManager(cfg),
ReadLockManager: fsutil.NewReadLockManager(),

View File

@ -10,6 +10,7 @@ import (
"runtime"
"time"
"github.com/remeh/sizedwaitgroup"
"github.com/stashapp/stash/internal/dlna"
"github.com/stashapp/stash/internal/log"
"github.com/stashapp/stash/internal/manager/config"
@ -33,6 +34,10 @@ type Manager struct {
Config *config.Config
Logger *log.Logger
// ImageThumbnailGenerateWaitGroup is the global wait group image thumbnail generation
// It uses the parallel tasks setting from the configuration.
ImageThumbnailGenerateWaitGroup sizedwaitgroup.SizedWaitGroup
Paths *paths.Paths
FFMpeg *ffmpeg.FFMpeg
@ -107,6 +112,8 @@ func (s *Manager) RefreshConfig() {
if err := fsutil.EnsureDir(s.Paths.Generated.InteractiveHeatmap); err != nil {
logger.Warnf("could not create interactive heatmaps directory: %v", err)
}
s.ImageThumbnailGenerateWaitGroup.Size = cfg.GetParallelTasksWithAutoDetection()
}
}

View File

@ -31,6 +31,7 @@ type GenerateMetadataInput struct {
Phashes bool `json:"phashes"`
InteractiveHeatmapsSpeeds bool `json:"interactiveHeatmapsSpeeds"`
ClipPreviews bool `json:"clipPreviews"`
ImageThumbnails bool `json:"imageThumbnails"`
// scene ids to generate for
SceneIDs []string `json:"sceneIDs"`
// marker ids to generate for
@ -60,6 +61,8 @@ type GenerateJob struct {
overwrite bool
fileNamingAlgo models.HashAlgorithm
totals totalsGenerate
}
type totalsGenerate struct {
@ -72,6 +75,7 @@ type totalsGenerate struct {
phashes int64
interactiveHeatmapSpeeds int64
clipPreviews int64
imageThumbnails int64
tasks int
}
@ -93,7 +97,6 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
go func() {
defer close(queue)
var totals totalsGenerate
sceneIDs, err := stringslice.StringSliceToIntSlice(j.input.SceneIDs)
if err != nil {
logger.Error(err.Error())
@ -116,7 +119,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
if err := r.WithReadTxn(ctx, func(ctx context.Context) error {
qb := r.Scene
if len(j.input.SceneIDs) == 0 && len(j.input.MarkerIDs) == 0 {
totals = j.queueTasks(ctx, g, queue)
j.queueTasks(ctx, g, queue)
} else {
if len(j.input.SceneIDs) > 0 {
scenes, err = qb.FindMany(ctx, sceneIDs)
@ -125,7 +128,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
return err
}
j.queueSceneJobs(ctx, g, s, queue, &totals)
j.queueSceneJobs(ctx, g, s, queue)
}
}
@ -135,7 +138,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
return err
}
for _, m := range markers {
j.queueMarkerJob(g, m, queue, &totals)
j.queueMarkerJob(g, m, queue)
}
}
}
@ -146,6 +149,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
return
}
totals := j.totals
logMsg := "Generating"
if j.input.Covers {
logMsg += fmt.Sprintf(" %d covers", totals.covers)
@ -174,6 +178,9 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
if j.input.ClipPreviews {
logMsg += fmt.Sprintf(" %d Image Clip Previews", totals.clipPreviews)
}
if j.input.ImageThumbnails {
logMsg += fmt.Sprintf(" %d Image Thumbnails", totals.imageThumbnails)
}
if logMsg == "Generating" {
logMsg = "Nothing selected to generate"
}
@ -223,9 +230,14 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
logger.Info(fmt.Sprintf("Generate finished (%s)", elapsed))
}
func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) totalsGenerate {
var totals totalsGenerate
func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) {
j.totals = totalsGenerate{}
j.queueScenesTasks(ctx, g, queue)
j.queueImagesTasks(ctx, g, queue)
}
func (j *GenerateJob) queueScenesTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) {
const batchSize = 1000
findFilter := models.BatchFindFilter(batchSize)
@ -234,26 +246,26 @@ func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, que
for more := true; more; {
if job.IsCancelled(ctx) {
return totals
return
}
scenes, err := scene.Query(ctx, r.Scene, nil, findFilter)
if err != nil {
logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
return totals
return
}
for _, ss := range scenes {
if job.IsCancelled(ctx) {
return totals
return
}
if err := ss.LoadFiles(ctx, r.Scene); err != nil {
logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
return totals
return
}
j.queueSceneJobs(ctx, g, ss, queue, &totals)
j.queueSceneJobs(ctx, g, ss, queue)
}
if len(scenes) != batchSize {
@ -262,30 +274,37 @@ func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, que
*findFilter.Page++
}
}
}
*findFilter.Page = 1
for more := j.input.ClipPreviews; more; {
func (j *GenerateJob) queueImagesTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) {
const batchSize = 1000
findFilter := models.BatchFindFilter(batchSize)
r := j.repository
for more := j.input.ClipPreviews || j.input.ImageThumbnails; more; {
if job.IsCancelled(ctx) {
return totals
return
}
images, err := image.Query(ctx, r.Image, nil, findFilter)
if err != nil {
logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
return totals
return
}
for _, ss := range images {
if job.IsCancelled(ctx) {
return totals
return
}
if err := ss.LoadFiles(ctx, r.Image); err != nil {
logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
return totals
return
}
j.queueImageJob(g, ss, queue, &totals)
j.queueImageJob(g, ss, queue)
}
if len(images) != batchSize {
@ -294,8 +313,6 @@ func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, que
*findFilter.Page++
}
}
return totals
}
func getGeneratePreviewOptions(optionsInput GeneratePreviewOptionsInput) generate.PreviewOptions {
@ -333,7 +350,7 @@ func getGeneratePreviewOptions(optionsInput GeneratePreviewOptionsInput) generat
return ret
}
func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, scene *models.Scene, queue chan<- Task, totals *totalsGenerate) {
func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, scene *models.Scene, queue chan<- Task) {
r := j.repository
if j.input.Covers {
@ -344,8 +361,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
}
if task.required(ctx) {
totals.covers++
totals.tasks++
j.totals.covers++
j.totals.tasks++
queue <- task
}
}
@ -358,8 +375,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
}
if task.required() {
totals.sprites++
totals.tasks++
j.totals.sprites++
j.totals.tasks++
queue <- task
}
}
@ -382,13 +399,13 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
if task.required() {
if task.videoPreviewRequired() {
totals.previews++
j.totals.previews++
}
if task.imagePreviewRequired() {
totals.imagePreviews++
j.totals.imagePreviews++
}
totals.tasks++
j.totals.tasks++
queue <- task
}
}
@ -407,8 +424,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
markers := task.markersNeeded(ctx)
if markers > 0 {
totals.markers += int64(markers)
totals.tasks++
j.totals.markers += int64(markers)
j.totals.tasks++
queue <- task
}
@ -424,8 +441,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
g: g,
}
if task.required() {
totals.transcodes++
totals.tasks++
j.totals.transcodes++
j.totals.tasks++
queue <- task
}
}
@ -441,8 +458,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
}
if task.required() {
totals.phashes++
totals.tasks++
j.totals.phashes++
j.totals.tasks++
queue <- task
}
}
@ -457,14 +474,14 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
}
if task.required() {
totals.interactiveHeatmapSpeeds++
totals.tasks++
j.totals.interactiveHeatmapSpeeds++
j.totals.tasks++
queue <- task
}
}
}
func (j *GenerateJob) queueMarkerJob(g *generate.Generator, marker *models.SceneMarker, queue chan<- Task, totals *totalsGenerate) {
func (j *GenerateJob) queueMarkerJob(g *generate.Generator, marker *models.SceneMarker, queue chan<- Task) {
task := &GenerateMarkersTask{
repository: j.repository,
Marker: marker,
@ -472,20 +489,35 @@ func (j *GenerateJob) queueMarkerJob(g *generate.Generator, marker *models.Scene
fileNamingAlgorithm: j.fileNamingAlgo,
generator: g,
}
totals.markers++
totals.tasks++
j.totals.markers++
j.totals.tasks++
queue <- task
}
func (j *GenerateJob) queueImageJob(g *generate.Generator, image *models.Image, queue chan<- Task, totals *totalsGenerate) {
task := &GenerateClipPreviewTask{
Image: *image,
Overwrite: j.overwrite,
func (j *GenerateJob) queueImageJob(g *generate.Generator, image *models.Image, queue chan<- Task) {
if j.input.ImageThumbnails {
task := &GenerateImageThumbnailTask{
Image: *image,
Overwrite: j.overwrite,
}
if task.required() {
j.totals.imageThumbnails++
j.totals.tasks++
queue <- task
}
}
if task.required() {
totals.clipPreviews++
totals.tasks++
queue <- task
if j.input.ClipPreviews {
task := &GenerateClipPreviewTask{
Image: *image,
Overwrite: j.overwrite,
}
if task.required() {
j.totals.clipPreviews++
j.totals.tasks++
queue <- task
}
}
}

View File

@ -0,0 +1,79 @@
package manager
import (
"context"
"errors"
"fmt"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
)
type GenerateImageThumbnailTask struct {
Image models.Image
Overwrite bool
}
func (t *GenerateImageThumbnailTask) GetDescription() string {
return fmt.Sprintf("Generating Thumbnail for image %s", t.Image.Path)
}
func (t *GenerateImageThumbnailTask) Start(ctx context.Context) {
if !t.required() {
return
}
thumbPath := GetInstance().Paths.Generated.GetThumbnailPath(t.Image.Checksum, models.DefaultGthumbWidth)
f := t.Image.Files.Primary()
path := f.Base().Path
logger.Debugf("Generating thumbnail for %s", path)
mgr := GetInstance()
c := mgr.Config
clipPreviewOptions := image.ClipPreviewOptions{
InputArgs: c.GetTranscodeInputArgs(),
OutputArgs: c.GetTranscodeOutputArgs(),
Preset: c.GetPreviewPreset().String(),
}
encoder := image.NewThumbnailEncoder(mgr.FFMpeg, mgr.FFProbe, clipPreviewOptions)
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
if err != nil {
// don't log for animated images
if !errors.Is(err, image.ErrNotSupportedForThumbnail) {
logger.Errorf("[generator] getting thumbnail for image %s: %w", path, err)
}
return
}
err = fsutil.WriteFile(thumbPath, data)
if err != nil {
logger.Errorf("[generator] writing thumbnail for image %s: %w", path, err)
return
}
}
func (t *GenerateImageThumbnailTask) required() bool {
vf, ok := t.Image.Files.Primary().(models.VisualFile)
if !ok {
return false
}
if vf.GetHeight() <= models.DefaultGthumbWidth && vf.GetWidth() <= models.DefaultGthumbWidth {
return false
}
if t.Overwrite {
return true
}
thumbPath := GetInstance().Paths.Generated.GetThumbnailPath(t.Image.Checksum, models.DefaultGthumbWidth)
exists, _ := fsutil.FileExists(thumbPath)
return !exists
}

View File

@ -2,7 +2,6 @@ package manager
import (
"context"
"errors"
"fmt"
"io/fs"
"path/filepath"
@ -412,9 +411,12 @@ func (g *imageGenerators) Generate(ctx context.Context, i *models.Image, f model
if t.ScanGenerateThumbnails {
// this should be quick, so always generate sequentially
if err := g.generateThumbnail(ctx, i, f); err != nil {
logger.Errorf("Error generating thumbnail for %s: %v", path, err)
taskThumbnail := GenerateImageThumbnailTask{
Image: *i,
Overwrite: overwrite,
}
taskThumbnail.Start(ctx)
}
// avoid adding a task if the file isn't a video file
@ -446,54 +448,6 @@ func (g *imageGenerators) Generate(ctx context.Context, i *models.Image, f model
return nil
}
func (g *imageGenerators) generateThumbnail(ctx context.Context, i *models.Image, f models.File) error {
thumbPath := g.paths.Generated.GetThumbnailPath(i.Checksum, models.DefaultGthumbWidth)
exists, _ := fsutil.FileExists(thumbPath)
if exists {
return nil
}
path := f.Base().Path
vf, ok := f.(models.VisualFile)
if !ok {
return fmt.Errorf("file %s is not a visual file", path)
}
if vf.GetHeight() <= models.DefaultGthumbWidth && vf.GetWidth() <= models.DefaultGthumbWidth {
return nil
}
logger.Debugf("Generating thumbnail for %s", path)
mgr := GetInstance()
c := mgr.Config
clipPreviewOptions := image.ClipPreviewOptions{
InputArgs: c.GetTranscodeInputArgs(),
OutputArgs: c.GetTranscodeOutputArgs(),
Preset: c.GetPreviewPreset().String(),
}
encoder := image.NewThumbnailEncoder(mgr.FFMpeg, mgr.FFProbe, clipPreviewOptions)
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
if err != nil {
// don't log for animated images
if !errors.Is(err, image.ErrNotSupportedForThumbnail) {
return fmt.Errorf("getting thumbnail for image %s: %w", path, err)
}
return nil
}
err = fsutil.WriteFile(thumbPath, data)
if err != nil {
return fmt.Errorf("writing thumbnail for image %s: %w", path, err)
}
return nil
}
type sceneGenerators struct {
input ScanMetadataInput
taskQueue *job.TaskQueue

View File

@ -18,6 +18,7 @@ type GenerateMetadataOptions struct {
Transcodes bool `json:"transcodes"`
Phashes bool `json:"phashes"`
InteractiveHeatmapsSpeeds bool `json:"interactiveHeatmapsSpeeds"`
ImageThumbnails bool `json:"imageThumbnails"`
ClipPreviews bool `json:"clipPreviews"`
}

View File

@ -196,6 +196,7 @@ fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult {
phashes
interactiveHeatmapsSpeeds
clipPreviews
imageThumbnails
}
deleteFile

View File

@ -17,11 +17,13 @@ import { SettingsContext } from "../Settings/context";
interface ISceneGenerateDialog {
selectedIds?: string[];
onClose: () => void;
type: "scene"; // TODO - add image generate
}
export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
selectedIds,
onClose,
type,
}) => {
const { configuration } = React.useContext(ConfigurationContext);
@ -200,6 +202,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
<SettingsContext>
<SettingSection>
<GenerateOptions
type={type}
options={options}
setOptions={setOptions}
selection

View File

@ -278,6 +278,7 @@ const ScenePage: React.FC<IProps> = ({
onClose={() => {
setIsGenerateDialogOpen(false);
}}
type="scene"
/>
);
}

View File

@ -235,6 +235,7 @@ export const SceneList: React.FC<ISceneList> = ({
if (isGenerateDialogOpen) {
return (
<GenerateDialog
type="scene"
selectedIds={Array.from(selectedIds.values())}
onClose={() => setIsGenerateDialogOpen(false)}
/>

View File

@ -7,12 +7,14 @@ import {
} from "../GeneratePreviewOptions";
interface IGenerateOptions {
type?: "scene" | "image";
selection?: boolean;
options: GQL.GenerateMetadataInput;
setOptions: (s: GQL.GenerateMetadataInput) => void;
}
export const GenerateOptions: React.FC<IGenerateOptions> = ({
type,
selection,
options,
setOptions: setOptionsState,
@ -24,136 +26,153 @@ export const GenerateOptions: React.FC<IGenerateOptions> = ({
setOptionsState({ ...options, ...input });
}
const showSceneOptions = !type || type === "scene";
const showImageOptions = !type || type === "image";
return (
<>
<BooleanSetting
id="covers-task"
headingID="dialogs.scene_gen.covers"
checked={options.covers ?? false}
onChange={(v) => setOptions({ covers: v })}
/>
<BooleanSetting
id="preview-task"
checked={options.previews ?? false}
headingID="dialogs.scene_gen.video_previews"
tooltipID="dialogs.scene_gen.video_previews_tooltip"
onChange={(v) => setOptions({ previews: v })}
/>
<BooleanSetting
advanced
className="sub-setting"
id="image-preview-task"
checked={options.imagePreviews ?? false}
disabled={!options.previews}
headingID="dialogs.scene_gen.image_previews"
tooltipID="dialogs.scene_gen.image_previews_tooltip"
onChange={(v) => setOptions({ imagePreviews: v })}
/>
{showSceneOptions && (
<>
<BooleanSetting
id="covers-task"
headingID="dialogs.scene_gen.covers"
checked={options.covers ?? false}
onChange={(v) => setOptions({ covers: v })}
/>
<BooleanSetting
id="preview-task"
checked={options.previews ?? false}
headingID="dialogs.scene_gen.video_previews"
tooltipID="dialogs.scene_gen.video_previews_tooltip"
onChange={(v) => setOptions({ previews: v })}
/>
<BooleanSetting
advanced
className="sub-setting"
id="image-preview-task"
checked={options.imagePreviews ?? false}
disabled={!options.previews}
headingID="dialogs.scene_gen.image_previews"
tooltipID="dialogs.scene_gen.image_previews_tooltip"
onChange={(v) => setOptions({ imagePreviews: v })}
/>
{/* #2251 - only allow preview generation options to be overridden when generating from a selection */}
{selection ? (
<ModalSetting<VideoPreviewSettingsInput>
id="video-preview-settings"
className="sub-setting"
disabled={!options.previews}
headingID="dialogs.scene_gen.override_preview_generation_options"
tooltipID="dialogs.scene_gen.override_preview_generation_options_desc"
value={{
previewExcludeEnd: previewOptions.previewExcludeEnd,
previewExcludeStart: previewOptions.previewExcludeStart,
previewSegmentDuration: previewOptions.previewSegmentDuration,
previewSegments: previewOptions.previewSegments,
}}
onChange={(v) => setOptions({ previewOptions: v })}
renderField={(value, setValue) => (
<VideoPreviewInput value={value ?? {}} setValue={setValue} />
)}
renderValue={() => {
return <></>;
}}
/>
) : undefined}
{/* #2251 - only allow preview generation options to be overridden when generating from a selection */}
{selection ? (
<ModalSetting<VideoPreviewSettingsInput>
id="video-preview-settings"
className="sub-setting"
disabled={!options.previews}
headingID="dialogs.scene_gen.override_preview_generation_options"
tooltipID="dialogs.scene_gen.override_preview_generation_options_desc"
value={{
previewExcludeEnd: previewOptions.previewExcludeEnd,
previewExcludeStart: previewOptions.previewExcludeStart,
previewSegmentDuration: previewOptions.previewSegmentDuration,
previewSegments: previewOptions.previewSegments,
}}
onChange={(v) => setOptions({ previewOptions: v })}
renderField={(value, setValue) => (
<VideoPreviewInput value={value ?? {}} setValue={setValue} />
)}
renderValue={() => {
return <></>;
}}
/>
) : undefined}
<BooleanSetting
id="sprite-task"
checked={options.sprites ?? false}
headingID="dialogs.scene_gen.sprites"
tooltipID="dialogs.scene_gen.sprites_tooltip"
onChange={(v) => setOptions({ sprites: v })}
/>
<BooleanSetting
id="marker-task"
checked={options.markers ?? false}
headingID="dialogs.scene_gen.markers"
tooltipID="dialogs.scene_gen.markers_tooltip"
onChange={(v) => setOptions({ markers: v })}
/>
<BooleanSetting
advanced
id="marker-image-preview-task"
className="sub-setting"
checked={options.markerImagePreviews ?? false}
disabled={!options.markers}
headingID="dialogs.scene_gen.marker_image_previews"
tooltipID="dialogs.scene_gen.marker_image_previews_tooltip"
onChange={(v) =>
setOptions({
markerImagePreviews: v,
})
}
/>
<BooleanSetting
advanced
id="marker-screenshot-task"
className="sub-setting"
checked={options.markerScreenshots ?? false}
disabled={!options.markers}
headingID="dialogs.scene_gen.marker_screenshots"
tooltipID="dialogs.scene_gen.marker_screenshots_tooltip"
onChange={(v) => setOptions({ markerScreenshots: v })}
/>
<BooleanSetting
id="sprite-task"
checked={options.sprites ?? false}
headingID="dialogs.scene_gen.sprites"
tooltipID="dialogs.scene_gen.sprites_tooltip"
onChange={(v) => setOptions({ sprites: v })}
/>
<BooleanSetting
id="marker-task"
checked={options.markers ?? false}
headingID="dialogs.scene_gen.markers"
tooltipID="dialogs.scene_gen.markers_tooltip"
onChange={(v) => setOptions({ markers: v })}
/>
<BooleanSetting
advanced
id="marker-image-preview-task"
className="sub-setting"
checked={options.markerImagePreviews ?? false}
disabled={!options.markers}
headingID="dialogs.scene_gen.marker_image_previews"
tooltipID="dialogs.scene_gen.marker_image_previews_tooltip"
onChange={(v) =>
setOptions({
markerImagePreviews: v,
})
}
/>
<BooleanSetting
advanced
id="marker-screenshot-task"
className="sub-setting"
checked={options.markerScreenshots ?? false}
disabled={!options.markers}
headingID="dialogs.scene_gen.marker_screenshots"
tooltipID="dialogs.scene_gen.marker_screenshots_tooltip"
onChange={(v) => setOptions({ markerScreenshots: v })}
/>
<BooleanSetting
advanced
id="transcode-task"
checked={options.transcodes ?? false}
headingID="dialogs.scene_gen.transcodes"
tooltipID="dialogs.scene_gen.transcodes_tooltip"
onChange={(v) => setOptions({ transcodes: v })}
/>
{selection ? (
<BooleanSetting
advanced
id="force-transcode"
className="sub-setting"
checked={options.forceTranscodes ?? false}
disabled={!options.transcodes}
headingID="dialogs.scene_gen.force_transcodes"
tooltipID="dialogs.scene_gen.force_transcodes_tooltip"
onChange={(v) => setOptions({ forceTranscodes: v })}
/>
) : undefined}
<BooleanSetting
advanced
id="transcode-task"
checked={options.transcodes ?? false}
headingID="dialogs.scene_gen.transcodes"
tooltipID="dialogs.scene_gen.transcodes_tooltip"
onChange={(v) => setOptions({ transcodes: v })}
/>
{selection ? (
<BooleanSetting
advanced
id="force-transcode"
className="sub-setting"
checked={options.forceTranscodes ?? false}
disabled={!options.transcodes}
headingID="dialogs.scene_gen.force_transcodes"
tooltipID="dialogs.scene_gen.force_transcodes_tooltip"
onChange={(v) => setOptions({ forceTranscodes: v })}
/>
) : undefined}
<BooleanSetting
id="phash-task"
checked={options.phashes ?? false}
headingID="dialogs.scene_gen.phash"
tooltipID="dialogs.scene_gen.phash_tooltip"
onChange={(v) => setOptions({ phashes: v })}
/>
<BooleanSetting
id="phash-task"
checked={options.phashes ?? false}
headingID="dialogs.scene_gen.phash"
tooltipID="dialogs.scene_gen.phash_tooltip"
onChange={(v) => setOptions({ phashes: v })}
/>
<BooleanSetting
id="interactive-heatmap-speed-task"
checked={options.interactiveHeatmapsSpeeds ?? false}
headingID="dialogs.scene_gen.interactive_heatmap_speed"
onChange={(v) => setOptions({ interactiveHeatmapsSpeeds: v })}
/>
<BooleanSetting
id="clip-previews"
checked={options.clipPreviews ?? false}
headingID="dialogs.scene_gen.clip_previews"
onChange={(v) => setOptions({ clipPreviews: v })}
/>
<BooleanSetting
id="interactive-heatmap-speed-task"
checked={options.interactiveHeatmapsSpeeds ?? false}
headingID="dialogs.scene_gen.interactive_heatmap_speed"
onChange={(v) => setOptions({ interactiveHeatmapsSpeeds: v })}
/>
</>
)}
{showImageOptions && (
<>
<BooleanSetting
id="clip-previews"
checked={options.clipPreviews ?? false}
headingID="dialogs.scene_gen.clip_previews"
onChange={(v) => setOptions({ clipPreviews: v })}
/>
<BooleanSetting
id="image-thumbnails"
checked={options.imageThumbnails ?? false}
headingID="dialogs.scene_gen.image_thumbnails"
onChange={(v) => setOptions({ imageThumbnails: v })}
/>
</>
)}
<BooleanSetting
id="overwrite"
checked={options.overwrite ?? false}

View File

@ -875,6 +875,7 @@
"force_transcodes_tooltip": "By default, transcodes are only generated when the video file is not supported in the browser. When enabled, transcodes will be generated even when the video file appears to be supported in the browser.",
"image_previews": "Animated Image Previews",
"image_previews_tooltip": "Also generate animated (webp) previews, only required when Scene/Marker Wall Preview Type is set to Animated Image. When browsing they use less CPU than the video previews, but are generated in addition to them and are larger files.",
"image_thumbnails": "Image Thumbnails",
"interactive_heatmap_speed": "Generate heatmaps and speeds for interactive scenes",
"marker_image_previews": "Marker Animated Image Previews",
"marker_image_previews_tooltip": "Also generate animated (webp) previews, only required when Scene/Marker Wall Preview Type is set to Animated Image. When browsing they use less CPU than the video previews, but are generated in addition to them and are larger files.",