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:
+27
-48
@@ -48,7 +48,7 @@ from . import bundle
|
||||
from .bundle import METADATA_FIELDS
|
||||
from .detect import deskew, detect_page
|
||||
from .pdf import Source, open_source, page_raster
|
||||
from .project import Cut, Project, default_path
|
||||
from .project import Cut, Project, open_project
|
||||
from .render import apply_levels
|
||||
|
||||
PREVIEW_MAX = 1800 # display resolution; geometry stays normalised
|
||||
@@ -356,17 +356,10 @@ class Editor(QMainWindow):
|
||||
discard = QPushButton("Toggle discard (D)")
|
||||
discard.clicked.connect(self.view.toggle_discard)
|
||||
form.addRow(discard)
|
||||
rect_row = QHBoxLayout()
|
||||
fit = QPushButton("Auto-fit")
|
||||
fit.setToolTip("Propose the content rectangle from the staff lines on this page")
|
||||
fit.clicked.connect(lambda: self._autofit_rect(False))
|
||||
fit_all = QPushButton("Auto-fit all")
|
||||
fit_all.clicked.connect(lambda: self._autofit_rect(True))
|
||||
reset = QPushButton("Reset")
|
||||
reset = QPushButton("Reset content rectangle")
|
||||
reset.setToolTip("Back to the rectangle detection proposed for this page")
|
||||
reset.clicked.connect(self._reset_rect)
|
||||
for button in (fit, fit_all, reset):
|
||||
rect_row.addWidget(button)
|
||||
form.addRow("Content rect", rect_row)
|
||||
form.addRow(reset)
|
||||
box.addWidget(page_box)
|
||||
|
||||
meta_box = QGroupBox("Song")
|
||||
@@ -482,25 +475,16 @@ class Editor(QMainWindow):
|
||||
}
|
||||
self.autosave.start()
|
||||
|
||||
def _autofit_rect(self, every_page: bool) -> None:
|
||||
"""Re-propose the content rectangle from the staff lines.
|
||||
|
||||
Detection does this for new projects; this is how a project made before
|
||||
it existed, or one whose rectangle was dragged wrong, gets it back.
|
||||
"""
|
||||
pages = range(len(self.project.pages)) if every_page else [self.index]
|
||||
for i in pages:
|
||||
# The preview is already deskewed, so the sweep is skipped.
|
||||
proposal = detect_page(self._preview(i), skew=0.0)
|
||||
self.project.pages[i].content_rect = proposal.content
|
||||
self.view.redraw()
|
||||
self._touched()
|
||||
self.statusBar().showMessage(
|
||||
f"content rectangle fitted on {len(list(pages))} page(s)", 2000
|
||||
)
|
||||
|
||||
def _reset_rect(self) -> None:
|
||||
self.project.pages[self.index].content_rect = None
|
||||
"""Back to what detection proposed for this page.
|
||||
|
||||
Not to the whole page: the proposal is what excludes the scan-edge
|
||||
junk, so clearing to full width would undo the thing the rectangle
|
||||
exists for. The preview is already deskewed, so the sweep is skipped.
|
||||
"""
|
||||
self.project.pages[self.index].content_rect = detect_page(
|
||||
self._preview(self.index), skew=0.0
|
||||
).content
|
||||
self.view.redraw()
|
||||
self._touched()
|
||||
|
||||
@@ -524,7 +508,9 @@ class Editor(QMainWindow):
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Exported",
|
||||
f"{out.name}\n{len(self.project.kept_slices())} slices, {size:.0f} KB",
|
||||
f"{out.name}\n{len(self.project.kept_slices())} slices, {size:.0f} KB\n\n"
|
||||
"This project is now spent — opening the PDF again starts a fresh "
|
||||
"session from detection.",
|
||||
)
|
||||
|
||||
def closeEvent(self, event) -> None:
|
||||
@@ -532,27 +518,20 @@ class Editor(QMainWindow):
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def launch(pdf: Path, source_type=None) -> int:
|
||||
def launch(pdf: Path, source_type=None, resume: bool = False) -> int:
|
||||
app = QApplication(sys.argv[:1])
|
||||
source = open_source(pdf, source_type)
|
||||
path = default_path(source.path)
|
||||
|
||||
if path.exists():
|
||||
project = Project.load(path)
|
||||
if project.source_changed():
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
"Source changed",
|
||||
"The PDF has changed since these cuts were made.\n"
|
||||
"Cuts may no longer line up with the music.",
|
||||
)
|
||||
else:
|
||||
detections, heights = [], []
|
||||
for i in range(len(source)):
|
||||
gray = page_raster(source, i)
|
||||
detections.append(detect_page(gray))
|
||||
heights.append(gray.shape[0])
|
||||
project = Project.from_detection(source.path, detections, heights)
|
||||
# An exported project is spent: this opens a fresh session from detection
|
||||
# rather than resuming decisions that have already been shipped.
|
||||
project = open_project(source, resume=resume)
|
||||
if project.path is not None and project.source_changed():
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
"Source changed",
|
||||
"The PDF has changed since these cuts were made.\n"
|
||||
"Cuts may no longer line up with the music.",
|
||||
)
|
||||
|
||||
window = Editor(source, project)
|
||||
window.resize(1500, 950)
|
||||
|
||||
Reference in New Issue
Block a user