*/ private $strategies = []; /** @var string */ private $format = ''; /** @var array */ private $ctx = []; /** @param array $ctx */ public function __construct(string $format, array $ctx = []) { $this->format = $format; $this->ctx = $ctx; } public function add_strategy(Import_Strategy $s): void { $this->strategies[] = $s; } /** * @return Import_Result */ public function run(): Import_Result { $best = new Import_Result([], 'none', 0.0, ['format' => $this->format]); $tries = []; foreach ($this->strategies as $s) { $chapters = []; $err = ''; try { $chapters = $s->run($this->ctx); } catch (\Throwable $e) { $err = $e->getMessage(); $chapters = []; } $score = $this->score($chapters); $tries[] = [ 'strategy' => $s->key(), 'score' => $score, 'chapters' => is_array($chapters) ? count($chapters) : 0, 'error' => $err, ]; if ($score > $best->score) { $best = new Import_Result($chapters, $s->key(), $score, ['tries' => $tries, 'format' => $this->format]); } // Early exit: very strong result. if ($best->score >= 85.0) { break; } } // Post-process best chapters for higher quality (TOC stripping, micro-chapter merging, header/footer cleanup). if (is_object($best) && isset($best->chapters) && is_array($best->chapters)) { $best->chapters = $this->postprocess($best->chapters); } // Always attach tries. if (!isset($best->meta['tries'])) { $best->meta['tries'] = $tries; } return $best; } /** * Post-process chapters after a strategy run. * * This is format-agnostic cleanup to improve chapter quality across TXT/DOCX/PDF/HTML/EPUB. * * - Strip embedded TOC blocks from content (when present) * - Remove repeated running headers/footers * - Merge tiny "micro-chapters" into the next chapter * * @param array $chapters * @return array */ private function postprocess(array $chapters): array { // Prefer the canonical implementation in Plugin when available so UI imports and engine imports stay consistent. if (class_exists('AIAAS\\Plugin') && method_exists('AIAAS\\Plugin', 'normalize_import_chapters')) { $rules = []; if (isset($this->ctx['rules']) && is_array($this->ctx['rules'])) { $rules = $this->ctx['rules']; } $fmt = $this->format; try { return (array) \AIAAS\Plugin::normalize_import_chapters($chapters, $rules, $fmt); } catch (\Throwable $e) { // fall through } } return $chapters; } /** * Score chapters (0-100). * * @param array $chapters */ private function score(array $chapters): float { $n = is_array($chapters) ? count($chapters) : 0; if ($n < 1) return 0.0; $titles = []; $total_words = 0; $good_titles = 0; $dup_titles = 0; $placeholder_titles = 0; $short_chapters = 0; foreach ($chapters as $ch) { $t = isset($ch['title']) ? trim((string)$ch['title']) : ''; $c = isset($ch['content']) ? trim((string)$ch['content']) : ''; $norm_t = strtolower(preg_replace('/\s+/', ' ', $t)); if ($norm_t !== '') { if (isset($titles[$norm_t])) $dup_titles++; $titles[$norm_t] = 1; } if ($t !== '' && strlen($t) >= 3 && strlen($t) <= 90) { // Penalize placeholders. if (preg_match('/^imported\s*\d+$/i', $t) || preg_match('/^section\s*\d+$/i', $t) || preg_match('/^page\s*\d+$/i', $t)) { $placeholder_titles++; } else { $good_titles++; } } $w = function_exists('str_word_count') ? (int) str_word_count($c) : (int) (strlen($c) / 5); $total_words += max(0, $w); if ($w > 0 && $w < 80) $short_chapters++; } // Base: chapter count preference. $score = 10.0; if ($n === 1) $score += 5.0; // sometimes correct if ($n >= 2) $score += 25.0; if ($n >= 5) $score += 20.0; if ($n >= 12) $score += 10.0; if ($n >= 40) $score -= 10.0; // too many = likely bad split // Coverage: need enough text. if ($total_words >= 800) $score += 10.0; if ($total_words >= 3000) $score += 10.0; if ($total_words < 200) $score -= 25.0; // Titles quality. if ($n > 0) { $score += 15.0 * ($good_titles / max(1, $n)); $score -= 20.0 * ($placeholder_titles / max(1, $n)); $score -= 10.0 * (min(1.0, $dup_titles / max(1, $n))); } // Too many tiny chapters. if ($n > 0) { $score -= 20.0 * ($short_chapters / max(1, $n)); } // Clamp if ($score < 0.0) $score = 0.0; if ($score > 100.0) $score = 100.0; return $score; } }