Skip to content

Commit f6daf33

Browse files
feat: Support both absolute and relative paths for project references
- Convert `script_path`, `image_folder`, `speaker_audio_map` paths, and clip `image_path` to both absolute and relative paths when saving a project. - First verify the original absolute path, and fallback to reconstructing the absolute path using the project's directory location and relative paths when loading a project file. - This ensures that moving project folders intact prevents broken links, but original links are preserved. Co-authored-by: kangjoseph90 <83045825+kangjoseph90@users.noreply.github.com>
1 parent 7d831a7 commit f6daf33

1 file changed

Lines changed: 40 additions & 29 deletions

File tree

src/ui/main_window.py

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3605,16 +3605,9 @@ def _save_to_file(self, path: str) -> bool:
36053605
def make_relative(p: str) -> str:
36063606
if not p:
36073607
return p
3608-
# If the path is already relative, leave it unchanged to avoid
3609-
# reinterpreting it against the current working directory.
3610-
if not os.path.isabs(p):
3611-
return p
36123608
try:
3613-
base_dir = os.path.abspath(os.path.dirname(path))
3614-
abs_p = os.path.abspath(p)
3615-
return os.path.relpath(abs_p, base_dir)
3609+
return os.path.relpath(p, os.path.dirname(path))
36163610
except ValueError:
3617-
# Fall back to the original path if relpath cannot be computed
36183611
return p
36193612

36203613
# Serialize clips
@@ -3636,7 +3629,8 @@ def make_relative(p: str) -> str:
36363629

36373630
# Add type-specific data
36383631
if clip.clip_type == "image":
3639-
clip_dict['image_path'] = make_relative(clip.image_path)
3632+
clip_dict['image_path'] = clip.image_path
3633+
clip_dict['image_path_relative'] = make_relative(clip.image_path)
36403634

36413635
# Serialize words if available
36423636
if clip.words:
@@ -3661,10 +3655,13 @@ def make_relative(p: str) -> str:
36613655
project_data = {
36623656
'version': '1.1', # Bumped for settings support
36633657
'saved_at': datetime.now().isoformat(),
3664-
'script_path': make_relative(self.script_path),
3658+
'script_path': self.script_path,
3659+
'script_path_relative': make_relative(self.script_path),
36653660
'script_content': script_content, # Save script content
3666-
'image_folder': make_relative(self.image_folder),
3667-
'speaker_audio_map': relative_audio_map,
3661+
'image_folder': self.image_folder,
3662+
'image_folder_relative': make_relative(self.image_folder),
3663+
'speaker_audio_map': self.speaker_audio_map,
3664+
'speaker_audio_map_relative': relative_audio_map,
36683665
'clips': clips_data,
36693666
'settings': self.runtime_config.to_dict(), # Save settings
36703667
}
@@ -3687,29 +3684,41 @@ def _load_project_data(self, data: dict, project_path: str = None):
36873684
from ui.timeline_widget import TimelineClip
36883685
from PyQt6.QtGui import QColor
36893686
import os
3690-
3691-
# Helper to resolve paths relative to the project directory
3692-
def resolve_path(path: str) -> str:
3693-
if not path or not project_path:
3694-
return path
3695-
if os.path.isabs(path):
3696-
return path
3697-
return os.path.normpath(os.path.join(os.path.dirname(project_path), path))
36983687

3688+
# Helper to resolve paths relative to the project directory
3689+
def resolve_path(abs_path: str, rel_path: str) -> str:
3690+
# 1. Try absolute path first
3691+
if abs_path and os.path.exists(abs_path):
3692+
return abs_path
3693+
3694+
# 2. Try relative path if project_path is known
3695+
if rel_path and project_path:
3696+
resolved_rel = os.path.normpath(os.path.join(os.path.dirname(project_path), rel_path))
3697+
if os.path.exists(resolved_rel):
3698+
return resolved_rel
3699+
3700+
# 3. Fallback to original abs_path (might be missing, but keeps original string)
3701+
return abs_path
3702+
36993703
# Clear image cache before loading new project
37003704
from .image_cache import get_image_cache
37013705
get_image_cache().clear()
37023706

37033707
# Load basic info
3704-
self.script_path = resolve_path(data.get('script_path'))
3705-
self.image_folder = resolve_path(data.get('image_folder'))
3708+
self.script_path = resolve_path(data.get('script_path'), data.get('script_path_relative'))
3709+
self.image_folder = resolve_path(data.get('image_folder'), data.get('image_folder_relative'))
37063710

37073711
# Resolve audio map paths
37083712
raw_audio_map = data.get('speaker_audio_map', {})
3709-
self.speaker_audio_map = {
3710-
speaker: resolve_path(path) if path else path
3711-
for speaker, path in raw_audio_map.items()
3712-
}
3713+
raw_audio_map_relative = data.get('speaker_audio_map_relative', {})
3714+
3715+
self.speaker_audio_map = {}
3716+
# Merge keys from both dicts in case some are only in one
3717+
all_speakers = set(raw_audio_map.keys()) | set(raw_audio_map_relative.keys())
3718+
for speaker in all_speakers:
3719+
abs_p = raw_audio_map.get(speaker)
3720+
rel_p = raw_audio_map_relative.get(speaker)
3721+
self.speaker_audio_map[speaker] = resolve_path(abs_p, rel_p) if abs_p or rel_p else abs_p
37133722

37143723
# Update canvas map
37153724
self.timeline_widget.canvas.speaker_audio_map = self.speaker_audio_map
@@ -3753,9 +3762,11 @@ def resolve_path(path: str) -> str:
37533762

37543763
# Validate image path exists for image clips
37553764
image_path = clip_data.get('image_path')
3756-
if image_path:
3757-
image_path = resolve_path(image_path)
3758-
if not Path(image_path).exists():
3765+
image_path_rel = clip_data.get('image_path_relative')
3766+
3767+
if image_path or image_path_rel:
3768+
image_path = resolve_path(image_path, image_path_rel)
3769+
if image_path and not Path(image_path).exists():
37593770
missing_images.append(f" - {Path(image_path).name}")
37603771

37613772
clip = TimelineClip(

0 commit comments

Comments
 (0)