Add comic panel detection system with multi-tier fallback

- panel-detection.service.ts: Main orchestration with OpenCV → ML → Grid fallback chain
- panel-detection.opencv.ts: Edge detection using OpenCV for 80% of comics
- panel-detection.ml.ts: COCO-SSD object detection for irregular layouts
- panel-detector.ts: Unified detector interface
- panel-editor.ts: Manual panel editor UI for user corrections
- panel-ml-detector.ts: TensorFlow.js integration for ML detection
- page-cache.ts: Efficient page caching for large comics
- background-color.ts: Auto-detect comic background color
- chapter-markers.ts: Chapter detection and navigation
- page-scrubber.ts: Fast page scrubbing/thumbnails
- page-order.ts: RTL/LTR page ordering support
- panel-gap.ts: Panel gap detection
This commit is contained in:
2026-04-04 01:00:43 -04:00
parent 169a8e7143
commit c36d8e6b57
12 changed files with 1381 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
// Page order presets for manga/comics
// Auto-detect Japanese vs Western reading order
// Allow user override in case detection is wrong
// Procedural implementation (no OOP)
type PageOrderMode = "auto" | "japanese" | "western";
interface PageOrderConfig {
mode: PageOrderMode;
detectedOrder: PageOrderMode;
userOverride: boolean;
}
interface PageOrderState {
config: PageOrderConfig;
totalPages: number;
}
// Detect page order based on filename patterns
function detectPageOrder(pageNames: string[]): PageOrderMode {
if (pageNames.length < 2) return "western";
const firstPage = pageNames[0].toLowerCase();
const lastPage = pageNames[pageNames.length - 1].toLowerCase();
const hasFrontCover = /cover|front|001/.test(firstPage);
const hasBackCover = /back|end|最后的/.test(lastPage);
if (hasFrontCover && !hasBackCover) {
return "western";
}
if (hasBackCover && !hasFrontCover) {
return "japanese";
}
const chapterMatches = pageNames.filter((n) => /ch-\d+|chapter/i.test(n));
if (chapterMatches.length > 0) {
const firstChapter = chapterMatches[0];
const pageNum = parseInt(firstChapter.match(/\d+/)?.[0] || "0");
return pageNum > 0 ? "western" : "japanese";
}
return "western";
}
function createPageOrderState(
totalPages: number,
pageNames: string[],
): PageOrderState {
const detectedOrder = detectPageOrder(pageNames);
return {
config: {
mode: "auto",
detectedOrder,
userOverride: false,
},
totalPages,
};
}
function setPageOrderMode(
state: PageOrderState,
mode: PageOrderMode,
): PageOrderState {
return {
...state,
config: {
...state.config,
mode,
userOverride: mode !== "auto",
},
};
}
function getPageOrder(state: PageOrderState): PageOrderMode {
if (state.config.mode === "auto") {
return state.config.detectedOrder;
}
return state.config.mode;
}
function reorderPages(state: PageOrderState, pageNumbers: number[]): number[] {
const order = getPageOrder(state);
if (order === "japanese") {
return [...pageNumbers].reverse();
}
return pageNumbers;
}
function getDisplayPageNumber(
state: PageOrderState,
actualPage: number,
): number {
const order = getPageOrder(state);
if (order === "japanese") {
return state.totalPages - actualPage + 1;
}
return actualPage;
}