Treat a project as spent once its song has been exported
Opening an exported song starts a fresh session from detection instead of resuming: cuts, discards and metadata do not carry over, so a re-cut never inherits decisions that have already shipped. --resume overrides it on edit, export and project. This reverses what was agreed in planning and written into docs/spec.md and CONTEXT.md, which promised resume-across-sessions and re-export. Both are corrected. The cost is deliberate and worth stating: changing the width cap or adding the SVG renderer later now means re-cutting each song by hand rather than regenerating every bundle from its project file. Export records the flag in bundle.write, so no caller can forget it. Also removed --refit and the Auto-fit buttons, which were added without being asked for and whose only purpose - migrating projects made before the content rectangle was proposed - disappears once exported projects start fresh. Reset now restores detection's proposal rather than the whole page: clearing to full width would undo the thing the rectangle exists for, so one button covers it. open_project() replaces four copies of load-or-detect across the CLI and the editor.
This commit is contained in:
@@ -113,6 +113,11 @@ class Project:
|
||||
levels: tuple[int, int] = (0, 255)
|
||||
metadata: dict[str, str] = field(default_factory=dict)
|
||||
path: Path | None = None
|
||||
# Set once the song has been exported. A project is spent at that point:
|
||||
# opening the PDF again starts a fresh session from detection rather than
|
||||
# resuming, so a re-cut never begins from stale decisions. `--resume`
|
||||
# overrides it when the old state really is wanted.
|
||||
exported: bool = False
|
||||
|
||||
# -- geometry helpers -------------------------------------------------
|
||||
|
||||
@@ -184,6 +189,7 @@ class Project:
|
||||
"v": FORMAT_VERSION,
|
||||
"source": self.source.name,
|
||||
"source_hash": self.source_hash,
|
||||
"exported": self.exported,
|
||||
"content_rect": list(self.content_rect),
|
||||
"levels": list(self.levels),
|
||||
"metadata": self.metadata,
|
||||
@@ -229,6 +235,7 @@ class Project:
|
||||
levels=tuple(data["levels"]),
|
||||
metadata=data.get("metadata", {}),
|
||||
path=path,
|
||||
exported=data.get("exported", False),
|
||||
)
|
||||
|
||||
def source_changed(self) -> bool:
|
||||
@@ -236,6 +243,30 @@ class Project:
|
||||
return self.source.exists() and hash_file(self.source) != self.source_hash
|
||||
|
||||
|
||||
def open_project(source, *, resume: bool = False) -> Project:
|
||||
"""The project for a PDF: resumed, or a fresh session from detection.
|
||||
|
||||
A project that has been exported is spent. Opening the PDF again starts
|
||||
over from detection rather than resuming, so a re-cut never inherits stale
|
||||
decisions. `resume` overrides that when the old state really is wanted.
|
||||
"""
|
||||
from .detect import detect_page
|
||||
from .pdf import page_raster
|
||||
|
||||
path = default_path(source.path)
|
||||
if path.exists():
|
||||
existing = Project.load(path)
|
||||
if resume or not existing.exported:
|
||||
return existing
|
||||
|
||||
detections, heights = [], []
|
||||
for i in range(len(source)):
|
||||
gray = page_raster(source, i)
|
||||
detections.append(detect_page(gray))
|
||||
heights.append(gray.shape[0])
|
||||
return Project.from_detection(source.path, detections, heights)
|
||||
|
||||
|
||||
def default_path(source: Path) -> Path:
|
||||
return Path(source).with_suffix(SUFFIX)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user