Compare commits
71 Commits
83ef71934b
...
master
Author | SHA1 | Date | |
---|---|---|---|
bd1824a7ca | |||
4806c95095 | |||
16c841d14d | |||
bfb9ed54d9 | |||
3ac725c2aa | |||
b5a0811cbd | |||
1ac8cd04b3 | |||
203d036a92 | |||
fa2ac22f9f | |||
2013ccf627 | |||
e1d94f2b24 | |||
9df6d73db8 | |||
01340a0a81 | |||
44ed4220b9 | |||
151744d144 | |||
e823a11929 | |||
c1c01e86ca | |||
184aceeee3 | |||
db2aa57ce5 | |||
92c2e62166 | |||
86c31a49d9 | |||
f5b8656bc2 | |||
b9c60ffc25 | |||
b6c7863b77 | |||
612d024161 | |||
840440eb1a | |||
c3bf49f301 | |||
192a5c7124 | |||
2246ef9f45 | |||
c52d9b9399 | |||
10284dad81 | |||
a2dc4a2186 | |||
5d76681ded | |||
f8acef2da4 | |||
65b80034cb | |||
5400592afd | |||
e6616ed1b1 | |||
048e8ef033 | |||
c08d5c5999 | |||
8c1efb1b05 | |||
f942392fb3 | |||
c749d9af80 | |||
71e5870306 | |||
e813be2890 | |||
80fb35cced | |||
d8b4439382 | |||
463228baf5 | |||
e7571a78f4 | |||
ea008ba23c | |||
366c338c5d | |||
0d26ffaca4 | |||
aaf78bf0da | |||
43d350fff2 | |||
d1b9e7c470 | |||
c50234f5c1 | |||
171155e528 | |||
710a1f7de3 | |||
13fbc45b74 | |||
8b4f8026cc | |||
5c66935157 | |||
bae760837c | |||
4a1649a568 | |||
ea1a6e58f4 | |||
0c3e5e21bf | |||
472efbb9d9 | |||
dd2f40460b | |||
b2c7cf11e9 | |||
e0e5c8d933 | |||
04e391551e | |||
f9f442a2d0 | |||
0fd108bc9a |
@@ -1,3 +1,14 @@
|
|||||||
module tcleaner
|
module tcleaner
|
||||||
|
|
||||||
go 1.23.6
|
go 1.23.6
|
||||||
|
|
||||||
|
require git.site.quack-lab.dev/dave/cylogger v1.4.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
|
github.com/hexops/valast v1.5.0 // indirect
|
||||||
|
golang.org/x/mod v0.7.0 // indirect
|
||||||
|
golang.org/x/sys v0.3.0 // indirect
|
||||||
|
golang.org/x/tools v0.4.0 // indirect
|
||||||
|
mvdan.cc/gofumpt v0.4.0 // indirect
|
||||||
|
)
|
||||||
|
28
cleaner/go.sum
Normal file
28
cleaner/go.sum
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
git.site.quack-lab.dev/dave/cylogger v1.4.0 h1:3Ca7V5JWvruARJd5S8xDFwW9LnZ9QInqkYLRdrEFvuY=
|
||||||
|
git.site.quack-lab.dev/dave/cylogger v1.4.0/go.mod h1:wctgZplMvroA4X6p8f4B/LaCKtiBcT1Pp+L14kcS8jk=
|
||||||
|
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||||
|
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/hexops/autogold v0.8.1 h1:wvyd/bAJ+Dy+DcE09BoLk6r4Fa5R5W+O+GUzmR985WM=
|
||||||
|
github.com/hexops/autogold v0.8.1/go.mod h1:97HLDXyG23akzAoRYJh/2OBs3kd80eHyKPvZw0S5ZBY=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
|
github.com/hexops/valast v1.5.0 h1:FBTuvVi0wjTngtXJRZXMbkN/Dn6DgsUsBwch2DUJU8Y=
|
||||||
|
github.com/hexops/valast v1.5.0/go.mod h1:Jcy1pNH7LNraVaAZDLyv21hHg2WBv9Nf9FL6fGxU7o4=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
|
||||||
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||||
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4=
|
||||||
|
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||||
|
mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM=
|
||||||
|
mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ=
|
@@ -1,7 +1,7 @@
|
|||||||
Windows Registry Editor Version 5.00
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
[HKEY_CURRENT_USER\Software\Classes\*\shell\Clean video name]
|
[HKEY_CURRENT_USER\Software\Classes\*\shell\Clean name]
|
||||||
@="Clean video name"
|
@="Clean name"
|
||||||
|
|
||||||
[HKEY_CURRENT_USER\Software\Classes\*\shell\Clean video name\command]
|
[HKEY_CURRENT_USER\Software\Classes\*\shell\Clean name\command]
|
||||||
@="C:\\Users\\administrator\\go\\bin\\tcleaner.exe \"%1\""
|
@="C:\\Users\\administrator\\go\\bin\\tcleaner.exe \"%1\""
|
@@ -6,51 +6,71 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
logger.InitFlag()
|
||||||
if flag.NArg() == 0 {
|
if flag.NArg() == 0 {
|
||||||
fmt.Println("Usage: cleaner <files>")
|
fmt.Println("Usage: cleaner <files>")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
// regex to match " - 2025-07-08 01h31m45s - "
|
// regex to match "2025-07-08"
|
||||||
re := regexp.MustCompile(` - (\d{4}-\d{2}-\d{2} \d{2}h\d{2}m\d{2}s) - `)
|
re := regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
|
||||||
|
editedRe := regexp.MustCompile(`_edited_\d{5}`)
|
||||||
|
|
||||||
for _, file := range flag.Args() {
|
for _, file := range flag.Args() {
|
||||||
|
filelog := logger.Default.WithPrefix(file)
|
||||||
|
filelog.Info("Processing file")
|
||||||
|
|
||||||
info, err := os.Stat(file)
|
info, err := os.Stat(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("ERROR: %v\n", err)
|
filelog.Error("ERROR: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
fmt.Printf("SKIP (directory): %s\n", file)
|
filelog.Info("SKIP (directory): %s\n", file)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
name := filepath.Base(file)
|
name := filepath.Base(file)
|
||||||
match := re.FindStringSubmatch(name)
|
match := re.FindStringSubmatch(name)
|
||||||
|
filelog.Debug("Match: %v", match)
|
||||||
if match == nil {
|
if match == nil {
|
||||||
fmt.Printf("SKIP (no date pattern): %s\n", name)
|
filelog.Info("SKIP (no date pattern): %s\n", name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namePart := match[0]
|
||||||
|
editMatch := editedRe.FindStringSubmatch(name)
|
||||||
|
filelog.Debug("Edit match: %v", editMatch)
|
||||||
|
if editMatch != nil {
|
||||||
|
namePart = namePart + editMatch[0]
|
||||||
|
filelog.Info("Video has edited part, new name: %s", namePart)
|
||||||
|
}
|
||||||
|
|
||||||
newName := match[1] + filepath.Ext(name)
|
newName := namePart + filepath.Ext(name)
|
||||||
|
filelog.Debug("New name: %s", newName)
|
||||||
if name == newName {
|
if name == newName {
|
||||||
fmt.Printf("SKIP (already named): %s\n", name)
|
filelog.Info("SKIP (already named): %s\n", name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filelog.Debug("Checking if target exists: %s", newName)
|
||||||
if _, err := os.Stat(newName); err == nil {
|
if _, err := os.Stat(newName); err == nil {
|
||||||
fmt.Printf("SKIP (target exists): %s -> %s\n", name, newName)
|
filelog.Info("SKIP (target exists): %s -> %s\n", name, newName)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filelog.Info("Renaming to: %s", newName)
|
||||||
err = os.Rename(name, newName)
|
err = os.Rename(name, newName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("ERROR renaming %s: %v\n", name, err)
|
filelog.Error("ERROR renaming %s: %v\n", name, err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("RENAMED: %s -> %s\n", name, newName)
|
filelog.Info("RENAMED: %s -> %s\n", name, newName)
|
||||||
}
|
}
|
||||||
|
filelog.Info("All done")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1578
croppa/main.py
1578
croppa/main.py
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,8 @@ readme = "README.md"
|
|||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"opencv-python>=4.8.0",
|
"opencv-python>=4.8.0",
|
||||||
"numpy>=1.24.0"
|
"numpy>=1.24.0",
|
||||||
|
"Pillow>=10.0.0"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
295
main.py
295
main.py
@@ -7,23 +7,74 @@ import argparse
|
|||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import subprocess
|
|
||||||
import json
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
class Cv2BufferedCap:
|
||||||
|
"""Buffered wrapper around cv2.VideoCapture that handles frame loading, seeking, and caching correctly"""
|
||||||
|
|
||||||
|
def __init__(self, video_path, backend=None):
|
||||||
|
self.video_path = video_path
|
||||||
|
self.cap = cv2.VideoCapture(str(video_path), backend)
|
||||||
|
if not self.cap.isOpened():
|
||||||
|
raise ValueError(f"Could not open video: {video_path}")
|
||||||
|
|
||||||
|
# Video properties
|
||||||
|
self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
self.fps = self.cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||||
|
self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||||
|
|
||||||
|
# Current position tracking
|
||||||
|
self.current_frame = 0
|
||||||
|
|
||||||
|
def get_frame(self, frame_number):
|
||||||
|
"""Get frame at specific index - always accurate"""
|
||||||
|
# Clamp frame number to valid range
|
||||||
|
frame_number = max(0, min(frame_number, self.total_frames - 1))
|
||||||
|
|
||||||
|
# Optimize for sequential reading (next frame)
|
||||||
|
if frame_number == self.current_frame + 1:
|
||||||
|
ret, frame = self.cap.read()
|
||||||
|
else:
|
||||||
|
# Seek for non-sequential access
|
||||||
|
self.cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
|
||||||
|
ret, frame = self.cap.read()
|
||||||
|
|
||||||
|
if ret:
|
||||||
|
self.current_frame = frame_number
|
||||||
|
return frame
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Failed to read frame {frame_number}")
|
||||||
|
|
||||||
|
def advance_frame(self, frames=1):
|
||||||
|
"""Advance by specified number of frames"""
|
||||||
|
new_frame = self.current_frame + frames
|
||||||
|
return self.get_frame(new_frame)
|
||||||
|
|
||||||
|
def release(self):
|
||||||
|
"""Release the video capture"""
|
||||||
|
if self.cap:
|
||||||
|
self.cap.release()
|
||||||
|
|
||||||
|
def isOpened(self):
|
||||||
|
"""Check if capture is opened"""
|
||||||
|
return self.cap and self.cap.isOpened()
|
||||||
|
|
||||||
|
|
||||||
class MediaGrader:
|
class MediaGrader:
|
||||||
BASE_FRAME_DELAY_MS = 16
|
# Configuration constants - matching croppa implementation
|
||||||
|
TARGET_FPS = 80 # Target FPS for speed calculations
|
||||||
|
SPEED_INCREMENT = 0.1
|
||||||
|
MIN_PLAYBACK_SPEED = 0.05
|
||||||
|
MAX_PLAYBACK_SPEED = 1.0
|
||||||
|
|
||||||
|
# Legacy constants for compatibility
|
||||||
KEY_REPEAT_RATE_SEC = 0.5
|
KEY_REPEAT_RATE_SEC = 0.5
|
||||||
FAST_SEEK_ACTIVATION_TIME = 2.0
|
FAST_SEEK_ACTIVATION_TIME = 2.0
|
||||||
FRAME_RENDER_TIME_MS = 50
|
|
||||||
SPEED_INCREMENT = 0.2
|
|
||||||
MIN_PLAYBACK_SPEED = 0.1
|
|
||||||
MAX_PLAYBACK_SPEED = 100.0
|
|
||||||
FAST_SEEK_MULTIPLIER = 60
|
FAST_SEEK_MULTIPLIER = 60
|
||||||
IMAGE_DISPLAY_DELAY_MS = 100
|
|
||||||
|
|
||||||
MONITOR_WIDTH = 2560
|
MONITOR_WIDTH = 2560
|
||||||
MONITOR_HEIGHT = 1440
|
MONITOR_HEIGHT = 1440
|
||||||
@@ -108,30 +159,53 @@ class MediaGrader:
|
|||||||
# Get frame dimensions
|
# Get frame dimensions
|
||||||
frame_height, frame_width = frame.shape[:2]
|
frame_height, frame_width = frame.shape[:2]
|
||||||
|
|
||||||
# Calculate aspect ratio
|
# Calculate available height (subtract timeline height for videos)
|
||||||
frame_aspect_ratio = frame_width / frame_height
|
timeline_height = self.TIMELINE_HEIGHT if self.is_video(self.media_files[self.current_index]) else 0
|
||||||
monitor_aspect_ratio = self.MONITOR_WIDTH / self.MONITOR_HEIGHT
|
available_height = self.MONITOR_HEIGHT - timeline_height
|
||||||
|
|
||||||
# Determine if frame is vertical or horizontal relative to monitor
|
# Calculate scale to fit within monitor bounds while maintaining aspect ratio
|
||||||
if frame_aspect_ratio < monitor_aspect_ratio:
|
scale_x = self.MONITOR_WIDTH / frame_width
|
||||||
# Frame is more vertical than monitor - maximize height
|
scale_y = available_height / frame_height
|
||||||
display_height = self.MONITOR_HEIGHT
|
scale = min(scale_x, scale_y)
|
||||||
display_width = int(display_height * frame_aspect_ratio)
|
|
||||||
|
# Calculate display dimensions
|
||||||
|
display_width = int(frame_width * scale)
|
||||||
|
display_height = int(frame_height * scale)
|
||||||
|
|
||||||
|
# Resize the frame to maintain aspect ratio
|
||||||
|
if scale != 1.0:
|
||||||
|
resized_frame = cv2.resize(frame, (display_width, display_height), interpolation=cv2.INTER_AREA)
|
||||||
else:
|
else:
|
||||||
# Frame is more horizontal than monitor - maximize width
|
resized_frame = frame
|
||||||
display_width = self.MONITOR_WIDTH
|
|
||||||
display_height = int(display_width / frame_aspect_ratio)
|
|
||||||
|
|
||||||
# Resize window to calculated dimensions
|
# Create canvas with proper dimensions
|
||||||
cv2.resizeWindow("Media Grader", display_width, display_height)
|
canvas_height = self.MONITOR_HEIGHT
|
||||||
|
canvas_width = self.MONITOR_WIDTH
|
||||||
|
canvas = np.zeros((canvas_height, canvas_width, 3), dtype=np.uint8)
|
||||||
|
|
||||||
|
# Center the resized frame on canvas
|
||||||
|
start_y = (available_height - display_height) // 2
|
||||||
|
start_x = (self.MONITOR_WIDTH - display_width) // 2
|
||||||
|
|
||||||
|
# Ensure frame fits within canvas bounds
|
||||||
|
end_y = min(start_y + display_height, available_height)
|
||||||
|
end_x = min(start_x + display_width, self.MONITOR_WIDTH)
|
||||||
|
actual_height = end_y - start_y
|
||||||
|
actual_width = end_x - start_x
|
||||||
|
|
||||||
|
if actual_height > 0 and actual_width > 0:
|
||||||
|
canvas[start_y:end_y, start_x:end_x] = resized_frame[:actual_height, :actual_width]
|
||||||
|
|
||||||
|
# Resize window to full monitor size
|
||||||
|
cv2.resizeWindow("Media Grader", self.MONITOR_WIDTH, self.MONITOR_HEIGHT)
|
||||||
|
|
||||||
# Center the window on screen
|
# Center the window on screen
|
||||||
x_position = (self.MONITOR_WIDTH - display_width) // 2
|
x_position = 0
|
||||||
y_position = (self.MONITOR_HEIGHT - display_height) // 2
|
y_position = 0
|
||||||
cv2.moveWindow("Media Grader", x_position, y_position)
|
cv2.moveWindow("Media Grader", x_position, y_position)
|
||||||
|
|
||||||
# Display the frame
|
# Display the canvas with properly aspect-ratioed frame
|
||||||
cv2.imshow("Media Grader", frame)
|
cv2.imshow("Media Grader", canvas)
|
||||||
|
|
||||||
def find_media_files(self) -> List[Path]:
|
def find_media_files(self) -> List[Path]:
|
||||||
"""Find all media files recursively in the directory"""
|
"""Find all media files recursively in the directory"""
|
||||||
@@ -158,19 +232,18 @@ class MediaGrader:
|
|||||||
|
|
||||||
def calculate_frame_delay(self) -> int:
|
def calculate_frame_delay(self) -> int:
|
||||||
"""Calculate frame delay in milliseconds based on playback speed"""
|
"""Calculate frame delay in milliseconds based on playback speed"""
|
||||||
delay_ms = int(self.BASE_FRAME_DELAY_MS / self.playback_speed)
|
# Round to 2 decimals to handle floating point precision issues
|
||||||
return max(1, delay_ms)
|
speed = round(self.playback_speed, 2)
|
||||||
|
if speed >= 1.0:
|
||||||
def calculate_frames_to_skip(self) -> int:
|
# Speed >= 1: maximum FPS (no delay)
|
||||||
"""Calculate how many frames to skip for high-speed playback"""
|
return 1
|
||||||
if self.playback_speed <= 1.0:
|
|
||||||
return 0
|
|
||||||
elif self.playback_speed <= 2.0:
|
|
||||||
return 0
|
|
||||||
elif self.playback_speed <= 5.0:
|
|
||||||
return int(self.playback_speed - 1)
|
|
||||||
else:
|
else:
|
||||||
return int(self.playback_speed * 2)
|
# Speed < 1: scale FPS based on speed
|
||||||
|
# Formula: fps = TARGET_FPS * speed, so delay = 1000 / fps
|
||||||
|
target_fps = self.TARGET_FPS * speed
|
||||||
|
delay_ms = int(1000 / target_fps)
|
||||||
|
return max(1, delay_ms)
|
||||||
|
|
||||||
|
|
||||||
def load_media(self, file_path: Path) -> bool:
|
def load_media(self, file_path: Path) -> bool:
|
||||||
"""Load media file for display"""
|
"""Load media file for display"""
|
||||||
@@ -178,43 +251,17 @@ class MediaGrader:
|
|||||||
self.current_cap.release()
|
self.current_cap.release()
|
||||||
|
|
||||||
if self.is_video(file_path):
|
if self.is_video(file_path):
|
||||||
# Try different backends for better performance
|
try:
|
||||||
# For video files: FFmpeg is usually best, DirectShow is for cameras
|
# Use Cv2BufferedCap for better frame handling
|
||||||
backends_to_try = []
|
self.current_cap = Cv2BufferedCap(file_path)
|
||||||
if hasattr(cv2, 'CAP_FFMPEG'): # FFmpeg - best for video files
|
self.total_frames = self.current_cap.total_frames
|
||||||
backends_to_try.append(cv2.CAP_FFMPEG)
|
self.current_frame = 0
|
||||||
if hasattr(cv2, 'CAP_DSHOW'): # DirectShow - usually for cameras, but try as fallback
|
|
||||||
backends_to_try.append(cv2.CAP_DSHOW)
|
|
||||||
backends_to_try.append(cv2.CAP_ANY) # Final fallback
|
|
||||||
|
|
||||||
self.current_cap = None
|
|
||||||
for backend in backends_to_try:
|
|
||||||
try:
|
|
||||||
self.current_cap = cv2.VideoCapture(str(file_path), backend)
|
|
||||||
if self.current_cap.isOpened():
|
|
||||||
# Optimize buffer settings for better performance
|
|
||||||
self.current_cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Minimize buffer to reduce latency
|
|
||||||
# Try to set hardware acceleration if available
|
|
||||||
if hasattr(cv2, 'CAP_PROP_HW_ACCELERATION'):
|
|
||||||
self.current_cap.set(cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY)
|
|
||||||
break
|
|
||||||
self.current_cap.release()
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not self.current_cap or not self.current_cap.isOpened():
|
|
||||||
print(f"Warning: Could not open video file {file_path.name} (unsupported codec)")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.total_frames = int(self.current_cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
print(f"Loaded: {file_path.name} | Frames: {self.total_frames} | FPS: {self.current_cap.fps:.2f}")
|
||||||
self.current_frame = 0
|
|
||||||
|
except Exception as e:
|
||||||
# Get codec information for debugging
|
print(f"Warning: Could not open video file {file_path.name}: {e}")
|
||||||
fourcc = int(self.current_cap.get(cv2.CAP_PROP_FOURCC))
|
return False
|
||||||
codec = "".join([chr((fourcc >> 8 * i) & 0xFF) for i in range(4)])
|
|
||||||
backend = self.current_cap.getBackendName()
|
|
||||||
|
|
||||||
print(f"Loaded: {file_path.name} | Codec: {codec} | Backend: {backend} | Frames: {self.total_frames}")
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.current_cap = None
|
self.current_cap = None
|
||||||
@@ -235,12 +282,13 @@ class MediaGrader:
|
|||||||
if not self.current_cap:
|
if not self.current_cap:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
ret, frame = self.current_cap.read()
|
try:
|
||||||
if ret:
|
# Use Cv2BufferedCap to get frame
|
||||||
self.current_display_frame = frame
|
self.current_display_frame = self.current_cap.get_frame(self.current_frame)
|
||||||
self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
|
||||||
return True
|
return True
|
||||||
return False
|
except Exception as e:
|
||||||
|
print(f"Failed to load frame {self.current_frame}: {e}")
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
frame = cv2.imread(str(self.media_files[self.current_index]))
|
frame = cv2.imread(str(self.media_files[self.current_index]))
|
||||||
if frame is not None:
|
if frame is not None:
|
||||||
@@ -672,7 +720,7 @@ class MediaGrader:
|
|||||||
# Draw timeline
|
# Draw timeline
|
||||||
self.draw_timeline(frame)
|
self.draw_timeline(frame)
|
||||||
|
|
||||||
# Maintain aspect ratio when displaying
|
# Display with proper aspect ratio
|
||||||
self.display_with_aspect_ratio(frame)
|
self.display_with_aspect_ratio(frame)
|
||||||
|
|
||||||
def display_multi_segment_frame(self):
|
def display_multi_segment_frame(self):
|
||||||
@@ -789,7 +837,7 @@ class MediaGrader:
|
|||||||
# Draw multi-segment timeline
|
# Draw multi-segment timeline
|
||||||
self.draw_multi_segment_timeline(combined_frame)
|
self.draw_multi_segment_timeline(combined_frame)
|
||||||
|
|
||||||
# Maintain aspect ratio when displaying
|
# Display with proper aspect ratio
|
||||||
self.display_with_aspect_ratio(combined_frame)
|
self.display_with_aspect_ratio(combined_frame)
|
||||||
|
|
||||||
def draw_multi_segment_timeline(self, frame):
|
def draw_multi_segment_timeline(self, frame):
|
||||||
@@ -904,38 +952,30 @@ class MediaGrader:
|
|||||||
target_frame = max(0, min(target_frame, self.total_frames - 1))
|
target_frame = max(0, min(target_frame, self.total_frames - 1))
|
||||||
|
|
||||||
# Seek to target frame
|
# Seek to target frame
|
||||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
|
self.current_frame = target_frame
|
||||||
self.load_current_frame()
|
self.load_current_frame()
|
||||||
|
|
||||||
def advance_frame(self):
|
def advance_frame(self):
|
||||||
"""Advance to next frame(s) based on playback speed"""
|
"""Advance to next frame - handles playback speed and marker looping"""
|
||||||
if (
|
if not self.is_playing:
|
||||||
not self.is_video(self.media_files[self.current_index])
|
return True
|
||||||
or not self.is_playing
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.multi_segment_mode:
|
if self.multi_segment_mode:
|
||||||
self.update_segment_frames()
|
self.update_segment_frames()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
frames_to_skip = self.calculate_frames_to_skip()
|
# Always advance by 1 frame - speed is controlled by delay timing
|
||||||
|
new_frame = self.current_frame + 1
|
||||||
|
|
||||||
for _ in range(frames_to_skip + 1):
|
# Handle looping bounds
|
||||||
ret, frame = self.current_cap.read()
|
if new_frame >= self.total_frames:
|
||||||
if not ret:
|
# Loop to beginning
|
||||||
actual_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
new_frame = 0
|
||||||
if actual_frame < self.total_frames - 5:
|
|
||||||
print(f"Frame count mismatch! Reported: {self.total_frames}, Actual: {actual_frame}")
|
|
||||||
self.total_frames = actual_frame
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.current_display_frame = frame
|
# Update current frame and load it
|
||||||
self.current_frame = int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
self.current_frame = new_frame
|
||||||
|
|
||||||
self.update_watch_tracking()
|
self.update_watch_tracking()
|
||||||
|
return self.load_current_frame()
|
||||||
return True
|
|
||||||
|
|
||||||
def seek_video(self, frames_delta: int):
|
def seek_video(self, frames_delta: int):
|
||||||
"""Seek video by specified number of frames"""
|
"""Seek video by specified number of frames"""
|
||||||
@@ -952,7 +992,7 @@ class MediaGrader:
|
|||||||
0, min(self.current_frame + frames_delta, self.total_frames - 1)
|
0, min(self.current_frame + frames_delta, self.total_frames - 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
|
self.current_frame = target_frame
|
||||||
self.load_current_frame()
|
self.load_current_frame()
|
||||||
|
|
||||||
def process_seek_key(self, key: int) -> bool:
|
def process_seek_key(self, key: int) -> bool:
|
||||||
@@ -1165,32 +1205,42 @@ class MediaGrader:
|
|||||||
cv2.setWindowTitle("Media Grader", window_title)
|
cv2.setWindowTitle("Media Grader", window_title)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
# Update display
|
||||||
self.display_current_frame()
|
self.display_current_frame()
|
||||||
|
|
||||||
if self.is_video(current_file):
|
# Calculate appropriate delay based on playback state
|
||||||
if self.is_seeking:
|
if self.is_playing and self.is_video(current_file):
|
||||||
delay = self.FRAME_RENDER_TIME_MS
|
# Use calculated frame delay for proper playback speed
|
||||||
else:
|
delay_ms = self.calculate_frame_delay()
|
||||||
delay = self.calculate_frame_delay()
|
|
||||||
else:
|
else:
|
||||||
delay = self.IMAGE_DISPLAY_DELAY_MS
|
# Use minimal delay for immediate responsiveness when not playing
|
||||||
|
delay_ms = 1
|
||||||
|
|
||||||
|
# Auto advance frame when playing (videos only)
|
||||||
|
if self.is_playing and self.is_video(current_file):
|
||||||
|
self.advance_frame()
|
||||||
|
|
||||||
key = cv2.waitKey(delay) & 0xFF
|
# Key capture with appropriate delay
|
||||||
|
key = cv2.waitKey(delay_ms) & 0xFF
|
||||||
|
|
||||||
if key == ord("q") or key == 27:
|
if key == ord("q") or key == 27:
|
||||||
return
|
return
|
||||||
elif key == ord(" "):
|
elif key == ord(" "):
|
||||||
self.is_playing = not self.is_playing
|
self.is_playing = not self.is_playing
|
||||||
elif key == ord("s"):
|
elif key == ord("s"):
|
||||||
self.playback_speed = max(
|
# Speed control only for videos
|
||||||
self.MIN_PLAYBACK_SPEED,
|
if self.is_video(current_file):
|
||||||
self.playback_speed - self.SPEED_INCREMENT,
|
self.playback_speed = max(
|
||||||
)
|
self.MIN_PLAYBACK_SPEED,
|
||||||
|
self.playback_speed - self.SPEED_INCREMENT,
|
||||||
|
)
|
||||||
elif key == ord("w"):
|
elif key == ord("w"):
|
||||||
self.playback_speed = min(
|
# Speed control only for videos
|
||||||
self.MAX_PLAYBACK_SPEED,
|
if self.is_video(current_file):
|
||||||
self.playback_speed + self.SPEED_INCREMENT,
|
self.playback_speed = min(
|
||||||
)
|
self.MAX_PLAYBACK_SPEED,
|
||||||
|
self.playback_speed + self.SPEED_INCREMENT,
|
||||||
|
)
|
||||||
elif self.process_seek_key(key):
|
elif self.process_seek_key(key):
|
||||||
continue
|
continue
|
||||||
elif key == ord("n"):
|
elif key == ord("n"):
|
||||||
@@ -1229,17 +1279,6 @@ class MediaGrader:
|
|||||||
if self.is_seeking and self.current_seek_key is not None:
|
if self.is_seeking and self.current_seek_key is not None:
|
||||||
self.process_seek_key(self.current_seek_key)
|
self.process_seek_key(self.current_seek_key)
|
||||||
|
|
||||||
if (
|
|
||||||
self.is_playing
|
|
||||||
and self.is_video(current_file)
|
|
||||||
and not self.is_seeking
|
|
||||||
):
|
|
||||||
if not self.advance_frame():
|
|
||||||
# Video reached the end, restart it instead of navigating
|
|
||||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
|
||||||
self.current_frame = 0
|
|
||||||
self.load_current_frame()
|
|
||||||
|
|
||||||
if key not in [ord("p"), ord("u"), ord("1"), ord("2"), ord("3"), ord("4"), ord("5")]:
|
if key not in [ord("p"), ord("u"), ord("1"), ord("2"), ord("3"), ord("4"), ord("5")]:
|
||||||
print("Navigating to (pu12345): ", self.current_index)
|
print("Navigating to (pu12345): ", self.current_index)
|
||||||
self.current_index += 1
|
self.current_index += 1
|
||||||
|
57
uv.lock
generated
57
uv.lock
generated
@@ -15,12 +15,14 @@ source = { virtual = "croppa" }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "numpy" },
|
{ name = "numpy" },
|
||||||
{ name = "opencv-python" },
|
{ name = "opencv-python" },
|
||||||
|
{ name = "pillow" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "numpy", specifier = ">=1.24.0" },
|
{ name = "numpy", specifier = ">=1.24.0" },
|
||||||
{ name = "opencv-python", specifier = ">=4.8.0" },
|
{ name = "opencv-python", specifier = ">=4.8.0" },
|
||||||
|
{ name = "pillow", specifier = ">=10.0.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -85,6 +87,61 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/fa/80/eb88edc2e2b11cd2dd2e56f1c80b5784d11d6e6b7f04a1145df64df40065/opencv_python-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:d98edb20aa932fd8ebd276a72627dad9dc097695b3d435a4257557bbb49a79d2", size = 39000307, upload-time = "2025-07-07T09:14:16.641Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/80/eb88edc2e2b11cd2dd2e56f1c80b5784d11d6e6b7f04a1145df64df40065/opencv_python-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:d98edb20aa932fd8ebd276a72627dad9dc097695b3d435a4257557bbb49a79d2", size = 39000307, upload-time = "2025-07-07T09:14:16.641Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pillow"
|
||||||
|
version = "11.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.12.12"
|
version = "0.12.12"
|
||||||
|
Reference in New Issue
Block a user