This commit is contained in:
parent
6ab2d3cdd1
commit
ab10516383
10
CHANGELOG.md
10
CHANGELOG.md
@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
|
||||
---
|
||||
|
||||
## [1.49.0] — 2026-09-22
|
||||
|
||||
### Added
|
||||
- **Magazine: a page-coverage gap finder.** Approve only ever advances to the next detected article, so a page never grouped into any article at all — an ad, a missed piece, anything detection skipped entirely — was never surfaced by that workflow. Confirmed live: 73 of 204 pages (36%) in one real issue belonged to no article whatsoever. The article list now shows a banner listing every such gap as page ranges, each a one-click jump straight into Review at that page (the review UI's own page navigation already isn't restricted to one article's pages — the missing piece was purely that nothing pointed there).
|
||||
|
||||
---
|
||||
|
||||
## [1.48.2] — 2026-09-22
|
||||
|
||||
### Changed
|
||||
@ -4379,7 +4386,8 @@ Initial feature-complete release.
|
||||
|
||||
---
|
||||
|
||||
[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.2...HEAD
|
||||
[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.49.0...HEAD
|
||||
[1.49.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.2...v1.49.0
|
||||
[1.48.2]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.1...v1.48.2
|
||||
[1.48.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.0...v1.48.1
|
||||
[1.48.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.47.1...v1.48.0
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#2563EB">
|
||||
<meta name="app-version" content="1.48.2">
|
||||
<meta name="app-version" content="1.49.0">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||||
@ -27,12 +27,12 @@
|
||||
|
||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.48.2">
|
||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.48.2">
|
||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.48.2">
|
||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.48.2">
|
||||
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.48.2">
|
||||
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.48.2">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.49.0">
|
||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.49.0">
|
||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.49.0">
|
||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.49.0">
|
||||
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.49.0">
|
||||
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.49.0">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -403,7 +403,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
||||
</script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.48.2"></script>
|
||||
<script src="/static/loader.js?v=1.49.0"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -619,6 +619,59 @@ function confidenceLabel(c) {
|
||||
return { cls: 'bad', text: Math.round(c * 100) + '% · little body copy — likely not an article', badge: Math.round(c * 100) + '%' };
|
||||
}
|
||||
|
||||
// ── Page coverage ────────────────────────────────────────────────────────
|
||||
// "Approve" only ever advances to the next ARTICLE, so a page that never
|
||||
// got grouped into any article -- an ad, a missed piece, a page the
|
||||
// detection pass just skipped -- is never surfaced by that workflow at all.
|
||||
// Confirmed live: 73 of 204 pages (36%) in one real issue belonged to no
|
||||
// article whatsoever. This finds those gaps and gives a one-click way into
|
||||
// Review at an arbitrary page, which the review UI already supports
|
||||
// (magReviewGoMagazinePage is not bounded to the anchor article's own
|
||||
// pages) -- the missing piece was purely that nothing pointed there.
|
||||
function magUncoveredPageRanges() {
|
||||
const total = state.doc?.pageCount || 0;
|
||||
if (!total) return [];
|
||||
const covered = new Set();
|
||||
state.articles.forEach((a) => (a.pages || []).forEach((p) => covered.add(p)));
|
||||
const gaps = [];
|
||||
let start = null;
|
||||
for (let p = 1; p <= total; p++) {
|
||||
if (!covered.has(p)) { if (start === null) start = p; }
|
||||
else if (start !== null) { gaps.push([start, p - 1]); start = null; }
|
||||
}
|
||||
if (start !== null) gaps.push([start, total]);
|
||||
return gaps;
|
||||
}
|
||||
|
||||
function renderCoverageBanner() {
|
||||
const el = $('mag-coverage-banner');
|
||||
if (!el) return;
|
||||
const gaps = magUncoveredPageRanges();
|
||||
if (!gaps.length) { el.hidden = true; el.innerHTML = ''; return; }
|
||||
const pageCount = gaps.reduce((n, [a, b]) => n + (b - a + 1), 0);
|
||||
el.hidden = false;
|
||||
el.innerHTML =
|
||||
'<span class="mdi mdi-alert-outline" aria-hidden="true"></span> ' +
|
||||
'<span>' + pageCount + ' page' + (pageCount === 1 ? '' : 's') + ' (' + gaps.length + ' gap' + (gaps.length === 1 ? '' : 's') +
|
||||
') ' + (pageCount === 1 ? "isn't" : "aren't") + ' part of any detected article, and will never come up just by approving articles one by one:</span>' +
|
||||
'<span class="mag-coverage-ranges">' +
|
||||
gaps.map(([a, b]) => '<button type="button" class="btn-sm" onclick="magReviewPage(' + a + ')">' +
|
||||
(a === b ? 'p. ' + a : 'pp. ' + a + '–' + b) + '</button>').join('') +
|
||||
'</span>';
|
||||
}
|
||||
|
||||
// Opens Review at a specific page even when no article covers it, by
|
||||
// anchoring on whichever approved-or-not article is nearest that page (the
|
||||
// review UI needs *an* article as context, but its own page navigation is
|
||||
// not restricted to that article's pages) and then jumping.
|
||||
async function magReviewPage(pageNumber) {
|
||||
if (!state.articles.length) return;
|
||||
const anchor = state.articles.slice().sort((a, b) =>
|
||||
Math.abs((a.pages?.[0] ?? 0) - pageNumber) - Math.abs((b.pages?.[0] ?? 0) - pageNumber))[0];
|
||||
await magReview(anchor.id);
|
||||
magReviewGoMagazinePage(pageNumber);
|
||||
}
|
||||
|
||||
function renderContents() {
|
||||
const host = $('mag-contents');
|
||||
if (!host) return;
|
||||
@ -631,6 +684,7 @@ function renderContents() {
|
||||
host.innerHTML = '<p class="muted">No articles yet. Run ingestion to detect them.</p>';
|
||||
return;
|
||||
}
|
||||
renderCoverageBanner();
|
||||
const bySection = new Map();
|
||||
state.articles.forEach((a) => {
|
||||
const key = a.section || 'Articles';
|
||||
@ -3452,6 +3506,7 @@ root.magContentsToggleSelect = magContentsToggleSelect;
|
||||
root.magContentsCommitTitle = magContentsCommitTitle;
|
||||
root.magGenerateAllAudio = magGenerateAllAudio;
|
||||
root.magTogglePlaylist = magTogglePlaylist;
|
||||
root.magReviewPage = magReviewPage;
|
||||
root.magPlaylistPlay = magPlaylistPlay;
|
||||
root.magContentsToggleArticle = magContentsToggleArticle;
|
||||
root.magContentsSelectAllVisible = magContentsSelectAllVisible;
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mag-coverage-banner" class="mag-coverage-banner" hidden></div>
|
||||
<div id="mag-contents-bulkbar" class="mag-bulkbar" hidden></div>
|
||||
<div class="mag-audio-export">
|
||||
<label for="mag-audio-voice">Narrator for exported MP3s</label>
|
||||
|
||||
@ -4930,6 +4930,12 @@ input[list="lang-codes"] {
|
||||
.mag-bulkbar-progress { flex: 1; min-width: 0; }
|
||||
.mag-bulkbar-actions { display: flex; gap: 6px; margin-left: auto; }
|
||||
|
||||
.mag-coverage-banner { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap;
|
||||
padding: 10px 14px; margin-bottom: 12px; border: 1px solid var(--yellow, #d97706); border-radius: var(--radius);
|
||||
background: color-mix(in srgb, var(--yellow, #d97706) 10%, var(--surface)); }
|
||||
.mag-coverage-banner .mdi { color: var(--yellow, #d97706); flex-shrink: 0; margin-top: 2px; }
|
||||
.mag-coverage-ranges { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
|
||||
.mag-audio-export { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
||||
padding: 10px 14px; margin-bottom: 12px; border: 1px solid var(--border); border-radius: var(--radius);
|
||||
background: var(--panel); }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user