Compare commits

...

11 Commits

Author SHA1 Message Date
Yiannis Christodoulou
ddf23271ea build assets 2025-12-09 09:56:27 +02:00
Yiannis Christodoulou
f2c83b8998 Bump version to 7.3.0
Update the VERSION in cms/version.py to 7.3.0 for the new release.
2025-12-09 09:55:13 +02:00
Yiannis Christodoulou
2c367d7eeb fix: Convert timeline header styles to CSS classes
Moved inline styles for timeline headers in chapters and video editors to dedicated CSS classes for better maintainability and consistency.
2025-12-08 17:19:41 +02:00
Yiannis Christodoulou
6eae3310ad feat: Improve Visual Distinction Between Trim and Chapters Editors 2025-12-08 17:05:23 +02:00
Yiannis Christodoulou
e536c74576 Update .gitignore 2025-12-08 16:39:30 +02:00
Markos Gogoulos
aeef8284bf docs: update page link 2025-12-01 11:29:58 +02:00
Markos Gogoulos
a90fcbf8dd version bump 2025-11-21 12:30:12 +02:00
Markos Gogoulos
1b3cdfd302 fix: add delay to task creation 2025-11-21 12:30:05 +02:00
Yiannis Christodoulou
cd7dd4f72c fix: Chapter numbering and preserve custom titles on segment reorder (#1435)
* FIX: Preserve custom chapter titles when renumbering (151)

Updated the renumberAllSegments function to only update chapter titles that match the default 'Chapter X' pattern, preserving any custom titles. Also ensured segments are renumbered after updates for consistent chronological naming.

* build assets (chapters editor)
2025-11-21 12:29:19 +02:00
Markos Gogoulos
9b3d9fe1e7 trim (#1431) 2025-11-13 12:42:48 +02:00
Markos Gogoulos
ea340b6a2e V7 f4 (#1430) 2025-11-13 12:30:25 +02:00
17 changed files with 138 additions and 79 deletions

1
.gitignore vendored
View File

@@ -35,3 +35,4 @@ frontend-tools/video-editor/client/public/videos/sample-video.mp3
frontend-tools/chapters-editor/client/public/videos/sample-video.mp3
static/chapters_editor/videos/sample-video.mp3
static/video_editor/videos/sample-video.mp3
templates/todo-MS4.md

View File

@@ -69,7 +69,7 @@ Copyright Markos Gogoulos.
## Support and paid services
We provide custom installations, development of extra functionality, migration from existing systems, integrations with legacy systems, training and support. Contact us at info@mediacms.io for more information.
We provide custom installations, development of extra functionality, migration from existing systems, integrations with legacy systems, training and support. Checkout our [services page](https://mediacms.io/#services/) for more information.
### Commercial Hostings
**Elestio**

View File

@@ -1 +1 @@
VERSION = "7.2.1"
VERSION = "7.3.0"

View File

@@ -910,7 +910,9 @@ def trim_video_method(media_file_path, timestamps_list):
return False
with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
output_file = os.path.join(temp_dir, "output.mp4")
# Detect input file extension to preserve original format
_, input_ext = os.path.splitext(media_file_path)
output_file = os.path.join(temp_dir, f"output{input_ext}")
segment_files = []
for i, item in enumerate(timestamps_list):
@@ -920,7 +922,7 @@ def trim_video_method(media_file_path, timestamps_list):
# For single timestamp, we can use the output file directly
# For multiple timestamps, we need to create segment files
segment_file = output_file if len(timestamps_list) == 1 else os.path.join(temp_dir, f"segment_{i}.mp4")
segment_file = output_file if len(timestamps_list) == 1 else os.path.join(temp_dir, f"segment_{i}{input_ext}")
cmd = [settings.FFMPEG_COMMAND, "-y", "-ss", str(item['startTime']), "-i", media_file_path, "-t", str(duration), "-c", "copy", "-avoid_negative_ts", "1", segment_file]

View File

@@ -494,7 +494,6 @@ def copy_video(original_media, copy_encodings=True, title_suffix="(Trimmed)"):
state=helpers.get_default_state(user=original_media.user),
is_reviewed=original_media.is_reviewed,
encoding_status=original_media.encoding_status,
listable=original_media.listable,
add_date=timezone.now(),
video_height=original_media.video_height,
size=original_media.size,
@@ -714,7 +713,6 @@ def copy_media(media):
state=helpers.get_default_state(user=media.user),
is_reviewed=media.is_reviewed,
encoding_status=media.encoding_status,
listable=media.listable,
add_date=timezone.now(),
)

View File

@@ -282,7 +282,7 @@ class Media(models.Model):
self.allow_whisper_transcribe != self.__original_allow_whisper_transcribe or self.allow_whisper_transcribe_and_translate != self.__original_allow_whisper_transcribe_and_translate
)
if transcription_changed and self.media_type == "video":
if transcription_changed and self.media_type in ["video", "audio"]:
self.transcribe_function()
# Update the original values for next comparison
@@ -329,10 +329,17 @@ class Media(models.Model):
if to_transcribe:
TranscriptionRequest.objects.create(media=self, translate_to_english=False)
tasks.whisper_transcribe.delay(self.friendly_token, translate_to_english=False)
tasks.whisper_transcribe.apply_async(
args=[self.friendly_token, False],
countdown=10,
)
if to_transcribe_and_translate:
TranscriptionRequest.objects.create(media=self, translate_to_english=True)
tasks.whisper_transcribe.delay(self.friendly_token, translate_to_english=True)
tasks.whisper_transcribe.apply_async(
args=[self.friendly_token, True],
countdown=10,
)
def update_search_vector(self):
"""

View File

@@ -150,6 +150,11 @@ const App = () => {
canRedo={historyPosition < history.length - 1}
/>
{/* Timeline Header */}
<div className="timeline-header-container">
<h2 className="timeline-header-title">Add Chapters</h2>
</div>
{/* Timeline Controls */}
<TimelineControls
currentTime={currentTime}

View File

@@ -20,7 +20,7 @@ const useVideoChapters = () => {
// Sort by start time to find chronological position
const sortedSegments = allSegments.sort((a, b) => a.startTime - b.startTime);
// Find the index of our new segment
const chapterIndex = sortedSegments.findIndex(seg => seg.startTime === newSegmentStartTime);
const chapterIndex = sortedSegments.findIndex((seg) => seg.startTime === newSegmentStartTime);
return `Chapter ${chapterIndex + 1}`;
};
@@ -30,10 +30,16 @@ const useVideoChapters = () => {
const sortedSegments = [...segments].sort((a, b) => a.startTime - b.startTime);
// Renumber each segment based on its chronological position
return sortedSegments.map((segment, index) => ({
...segment,
chapterTitle: `Chapter ${index + 1}`
}));
// Only update titles that follow the default "Chapter X" pattern to preserve custom titles
return sortedSegments.map((segment, index) => {
const currentTitle = segment.chapterTitle || '';
const isDefaultTitle = /^Chapter \d+$/.test(currentTitle);
return {
...segment,
chapterTitle: isDefaultTitle ? `Chapter ${index + 1}` : currentTitle,
};
});
};
// Helper function to parse time string (HH:MM:SS.mmm) to seconds
@@ -124,9 +130,7 @@ const useVideoChapters = () => {
let initialSegments: Segment[] = [];
// Check if we have existing chapters from the backend
const existingChapters =
(typeof window !== 'undefined' && (window as any).MEDIA_DATA?.chapters) ||
[];
const existingChapters = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.chapters) || [];
if (existingChapters.length > 0) {
// Create segments from existing chapters
@@ -564,8 +568,11 @@ const useVideoChapters = () => {
`Updating segments with action: ${actionType}, recordHistory: ${isSignificantChange ? 'true' : 'false'}`
);
// Renumber all segments to ensure proper chronological naming
const renumberedSegments = renumberAllSegments(e.detail.segments);
// Update segment state immediately for UI feedback
setClipSegments(e.detail.segments);
setClipSegments(renumberedSegments);
// Always save state to history for non-intermediate actions
if (isSignificantChange) {
@@ -573,7 +580,7 @@ const useVideoChapters = () => {
// ensure we capture the state properly
setTimeout(() => {
// Deep clone to ensure state is captured correctly
const segmentsClone = JSON.parse(JSON.stringify(e.detail.segments));
const segmentsClone = JSON.parse(JSON.stringify(renumberedSegments));
// Create a complete state snapshot
const stateWithAction: EditorState = {

View File

@@ -1,4 +1,16 @@
#chapters-editor-root {
.timeline-header-container {
margin-left: 1rem;
margin-top: -0.5rem;
}
.timeline-header-title {
font-size: 1.125rem;
font-weight: 600;
color: #059669;
margin: 0;
}
.timeline-container-card {
background-color: white;
border-radius: 0.5rem;
@@ -11,6 +23,8 @@
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0.5rem;
border-bottom: 2px solid rgba(16, 185, 129, 0.2);
}
.timeline-title {
@@ -21,6 +35,8 @@
.timeline-title-text {
font-weight: 700;
color: #059669;
font-size: 0.875rem;
}
.current-time {
@@ -48,10 +64,11 @@
.timeline-container {
position: relative;
min-width: 100%;
background-color: #fafbfc;
background-color: #E2EDE4;
height: 70px;
border-radius: 0.25rem;
overflow: visible !important;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.timeline-marker {
@@ -194,7 +211,7 @@
left: 0;
right: 0;
padding: 0.4rem;
background-color: rgba(0, 0, 0, 0.4);
background-color: rgba(16, 185, 129, 0.6);
color: white;
opacity: 1;
transition: background-color 0.2s;
@@ -202,15 +219,15 @@
}
.clip-segment:hover .clip-segment-info {
background-color: rgba(0, 0, 0, 0.5);
background-color: rgba(16, 185, 129, 0.7);
}
.clip-segment.selected .clip-segment-info {
background-color: rgba(59, 130, 246, 0.5);
background-color: rgba(5, 150, 105, 0.8);
}
.clip-segment.selected:hover .clip-segment-info {
background-color: rgba(59, 130, 246, 0.4);
background-color: rgba(5, 150, 105, 0.75);
}
.clip-segment-name {

View File

@@ -309,6 +309,11 @@ const App = () => {
canRedo={historyPosition < history.length - 1}
/>
{/* Timeline Header */}
<div className="timeline-header-container">
<h2 className="timeline-header-title">Trim or Split</h2>
</div>
{/* Timeline Controls */}
<TimelineControls
currentTime={currentTime}

View File

@@ -1,4 +1,16 @@
#video-editor-trim-root {
.timeline-header-container {
margin-left: 1rem;
margin-top: -0.5rem;
}
.timeline-header-title {
font-size: 1.125rem;
font-weight: 600;
color: #2563eb;
margin: 0;
}
.timeline-container-card {
background-color: white;
border-radius: 0.5rem;
@@ -11,6 +23,8 @@
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0.5rem;
border-bottom: 2px solid rgba(59, 130, 246, 0.2);
}
.timeline-title {
@@ -21,6 +35,8 @@
.timeline-title-text {
font-weight: 700;
color: #2563eb;
font-size: 0.875rem;
}
.current-time {
@@ -48,10 +64,11 @@
.timeline-container {
position: relative;
min-width: 100%;
background-color: #fafbfc;
background-color: #eff6ff;
height: 70px;
border-radius: 0.25rem;
overflow: visible !important;
border: 1px solid rgba(59, 130, 246, 0.2);
}
.timeline-marker {
@@ -194,7 +211,7 @@
left: 0;
right: 0;
padding: 0.4rem;
background-color: rgba(0, 0, 0, 0.4);
background-color: rgba(59, 130, 246, 0.6);
color: white;
opacity: 1;
transition: background-color 0.2s;
@@ -202,15 +219,15 @@
}
.clip-segment:hover .clip-segment-info {
background-color: rgba(0, 0, 0, 0.5);
background-color: rgba(59, 130, 246, 0.7);
}
.clip-segment.selected .clip-segment-info {
background-color: rgba(59, 130, 246, 0.5);
background-color: rgba(37, 99, 235, 0.8);
}
.clip-segment.selected:hover .clip-segment-info {
background-color: rgba(59, 130, 246, 0.4);
background-color: rgba(37, 99, 235, 0.75);
}
.clip-segment-name {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long