#!/usr/bin/env python3 from __future__ import annotations import json import re import sys import webbrowser from html import escape from pathlib import Path _REPOSITORY_ROOT_DIRECTORY = Path(__file__).resolve().parent.parent.parent _NOTE_DETAIL_OUTPUT_ROOT_DIRECTORY = _REPOSITORY_ROOT_DIRECTORY / "output" / "note-detail" _NOTE_OUTPUT_ROOT_DIRECTORY = _REPOSITORY_ROOT_DIRECTORY / "output" / "note" _PREVIEW_HTML_OUTPUT_FILE_PATH = _NOTE_DETAIL_OUTPUT_ROOT_DIRECTORY / "preview-note-detail.html" _PREVIEW_HTML_DOCUMENT_PAGE_TITLE_VISIBLE_TEXT = "小红书笔记预览" def _natural_sort_detail_image_file_path_list( detail_note_images_directory_path: Path, ) -> list[Path]: if not detail_note_images_directory_path.is_dir(): return [] pattern = re.compile(r"detail_note_image_(\d+)", re.IGNORECASE) candidates: list[tuple[tuple[int, ...], Path]] = [] for image_file_path in detail_note_images_directory_path.iterdir(): if not image_file_path.is_file(): continue match = pattern.search(image_file_path.name) key = (int(match.group(1)),) if match else (0, image_file_path.name) candidates.append((key, image_file_path)) candidates.sort(key=lambda item: item[0]) return [path for _key, path in candidates] def _read_note_detail_title_text(note_detail_sequence_directory_path: Path) -> str: note_detail_title_file_path = note_detail_sequence_directory_path / "note_detail_title.txt" if not note_detail_title_file_path.is_file(): return "" return note_detail_title_file_path.read_text(encoding="utf-8").strip() def _read_note_detail_body_metadata_dictionary( note_detail_sequence_directory_path: Path, ) -> dict[str, object]: note_detail_body_json_file_path = note_detail_sequence_directory_path / "note_detail_body.json" if not note_detail_body_json_file_path.is_file(): return {} return json.loads(note_detail_body_json_file_path.read_text(encoding="utf-8")) def _read_note_card_metadata_dictionary( note_output_sequence_directory_path: Path, ) -> dict[str, object]: note_card_metadata_json_file_path = note_output_sequence_directory_path / "note_card_metadata.json" if not note_card_metadata_json_file_path.is_file(): return {} return json.loads(note_card_metadata_json_file_path.read_text(encoding="utf-8")) def _build_html_document_fragment_for_one_note_detail_sequence( note_detail_sequence_directory_path: Path, note_detail_sequence_directory_name: str, note_card_metadata_dictionary: dict[str, object], ) -> str: title_text = _read_note_detail_title_text(note_detail_sequence_directory_path) body_metadata_dictionary = _read_note_detail_body_metadata_dictionary( note_detail_sequence_directory_path, ) detail_body_plain_text = str( body_metadata_dictionary.get("detail_body_plain_text") or "", ) detail_body_hashtag_object_list: list[object] = list( body_metadata_dictionary.get("detail_body_hashtag_list") or [], ) detail_body_emoji_image_url_list: list[object] = list( body_metadata_dictionary.get("detail_body_emoji_image_url_list") or [], ) card_note_title_text = str(note_card_metadata_dictionary.get("note_title") or "").strip() author_name_text = str(note_card_metadata_dictionary.get("author_name") or "").strip() publish_time_text = str(note_card_metadata_dictionary.get("publish_time") or "").strip() likes_count_text = str(note_card_metadata_dictionary.get("likes_count") or "").strip() display_title_text = title_text if title_text else card_note_title_text detail_note_images_directory_path = ( note_detail_sequence_directory_path / "detail_note_images" ) image_file_path_list = _natural_sort_detail_image_file_path_list( detail_note_images_directory_path, ) image_relative_path_string_list: list[str] = [] for image_file_path in image_file_path_list: relative_path = Path(note_detail_sequence_directory_name) / "detail_note_images" / image_file_path.name image_relative_path_string_list.append( relative_path.as_posix(), ) display_title_html = escape(display_title_text) if display_title_text else "" author_name_html = escape(author_name_text) if author_name_text else "" publish_time_html = escape(publish_time_text) if publish_time_text else "" likes_count_html = escape(likes_count_text) if likes_count_text else "" hashtag_link_html_fragment_list: list[str] = [] for hashtag_object in detail_body_hashtag_object_list: if not isinstance(hashtag_object, dict): continue display_text = str(hashtag_object.get("display_text") or "").strip() relative_href = str(hashtag_object.get("relative_href") or "").strip() if not display_text: continue if relative_href.startswith("/"): full_href = f"https://www.xiaohongshu.com{relative_href}" elif relative_href.startswith("http"): full_href = relative_href else: full_href = relative_href hashtag_link_html_fragment_list.append( f'{escape(display_text)}', ) body_html = "" if detail_body_plain_text.strip(): body_html = ( f'
{escape(detail_body_plain_text)}
' ) hashtag_section_html = "" if not detail_body_plain_text.strip() and hashtag_link_html_fragment_list: hashtag_section_html = ( '
' + " ".join(hashtag_link_html_fragment_list) + "
" ) emoji_row_html_fragment_list: list[str] = [] for emoji_image_url_object in detail_body_emoji_image_url_list: emoji_url = str(emoji_image_url_object or "").strip() if not emoji_url.startswith("http"): continue emoji_row_html_fragment_list.append( f'', ) emoji_section_html = "" if emoji_row_html_fragment_list: emoji_section_html = ( '
' + "".join(emoji_row_html_fragment_list) + "
" ) carousel_slide_html_fragment_list: list[str] = [] for image_relative_path_string in image_relative_path_string_list: carousel_slide_html_fragment_list.append( '", ) has_local_detail_image_file_for_carousel = bool(carousel_slide_html_fragment_list) slides_inner_html = "".join(carousel_slide_html_fragment_list) slide_count_integer = len(carousel_slide_html_fragment_list) article_id_attribute_value = escape(note_detail_sequence_directory_name, quote=True) meta_heading_block_html = "" if author_name_html: meta_heading_block_html += ( f'
{author_name_html}
' ) if display_title_html: meta_heading_block_html += f'

{display_title_html}

' stats_row_html_fragment_list: list[str] = [] if likes_count_html: stats_row_html_fragment_list.append( f'' f'{likes_count_html}', ) if publish_time_html: stats_row_html_fragment_list.append( f'{publish_time_html}', ) stats_row_html = "" if stats_row_html_fragment_list: stats_row_html = ( '
' + "".join(stats_row_html_fragment_list) + "
" ) meta_body_inner_html_fragment_list: list[str] = [] if body_html: meta_body_inner_html_fragment_list.append(body_html) if emoji_section_html: meta_body_inner_html_fragment_list.append(emoji_section_html) if hashtag_section_html: meta_body_inner_html_fragment_list.append(hashtag_section_html) meta_body_block_html = "" if meta_body_inner_html_fragment_list: meta_body_block_html = ( '
' + "".join(meta_body_inner_html_fragment_list) + "
" ) note_detail_carousel_column_html = "" if has_local_detail_image_file_for_carousel: carousel_dot_button_html_fragment_list: list[str] = [] for carousel_dot_zero_based_index in range(slide_count_integer): carousel_dot_active_css_class_suffix = ( " is-active" if carousel_dot_zero_based_index == 0 else "" ) carousel_dot_button_html_fragment_list.append( '', ) carousel_dots_inner_html = "".join(carousel_dot_button_html_fragment_list) carousel_counter_text_initial = f"1/{slide_count_integer}" note_detail_carousel_column_html = f"""""" meta_card_inner_html = ( meta_heading_block_html + stats_row_html + meta_body_block_html ) shell_modifier_class_list: list[str] = [] if not has_local_detail_image_file_for_carousel: shell_modifier_class_list.append("note-detail-shell--no-carousel") if has_local_detail_image_file_for_carousel and not meta_card_inner_html.strip(): shell_modifier_class_list.append("note-detail-shell--carousel-only") if not has_local_detail_image_file_for_carousel and meta_card_inner_html.strip(): shell_modifier_class_list.append("note-detail-shell--meta-only") shell_modifier_class_html = ( " " + " ".join(shell_modifier_class_list) if shell_modifier_class_list else "" ) note_detail_meta_column_html = "" if meta_card_inner_html.strip(): note_detail_meta_column_html = f"""
{meta_card_inner_html}
""" if not note_detail_carousel_column_html and not note_detail_meta_column_html: return f"""
""" return f"""
{note_detail_carousel_column_html} {note_detail_meta_column_html}
""" def _build_full_html_document_string( note_detail_sequence_directory_path_list: list[Path], note_card_metadata_by_sequence_name_dictionary: dict[str, dict[str, object]], ) -> str: fragment_list: list[str] = [] for note_detail_sequence_directory_path in note_detail_sequence_directory_path_list: sequence_name = note_detail_sequence_directory_path.name note_card_metadata_dictionary = note_card_metadata_by_sequence_name_dictionary.get( sequence_name, {}, ) fragment_list.append( _build_html_document_fragment_for_one_note_detail_sequence( note_detail_sequence_directory_path, sequence_name, note_card_metadata_dictionary, ), ) inner = "\n".join(fragment_list) page_title_escaped = escape(_PREVIEW_HTML_DOCUMENT_PAGE_TITLE_VISIBLE_TEXT) stylesheet = """ :root { color-scheme: light dark; --xhs-red: #ff2442; --xhs-red-soft: rgba(255, 36, 66, 0.12); --panel-bg: #fff; --page-bg-top: #f8f6f7; --page-bg-bottom: #eef0f5; --text: #1a1a1a; --muted: rgba(0,0,0,0.48); --radius-lg: 18px; --shadow-card: 0 4px 24px rgba(15, 23, 42, 0.08), 0 1px 3px rgba(15, 23, 42, 0.04); --shadow-card-hover: 0 8px 32px rgba(15, 23, 42, 0.1); } * { box-sizing: border-box; } html { width: 100vw; height: 100vh; max-width: 100vw; max-height: 100vh; overflow: hidden; } body { margin: 0; width: 100vw; height: 100vh; max-width: 100vw; max-height: 100vh; overflow: hidden; display: flex; flex-direction: column; font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif; color: var(--text); background: linear-gradient(168deg, var(--page-bg-top) 0%, var(--page-bg-bottom) 55%, #e6e9f0 100%); background-attachment: fixed; letter-spacing: 0.01em; } .preview-page-header { flex: 0 0 auto; width: min(1080px, 100vw); max-width: 100%; margin: 0 auto; padding: clamp(12px, 2.8vh, 28px) clamp(16px, 2.5vw, 20px) clamp(6px, 1vh, 8px); } .preview-page-header-inner { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; } .preview-page-title { margin: 0; font-size: clamp(1.35rem, 3.5vw, 1.65rem); font-weight: 700; letter-spacing: 0.04em; line-height: 1.25; display: flex; align-items: center; gap: 12px; } .preview-page-title-mark { display: inline-block; width: 4px; height: 1.15em; border-radius: 2px; background: linear-gradient(180deg, var(--xhs-red) 0%, #ff6b6b 100%); flex-shrink: 0; } .preview-page-title-text { background: linear-gradient(90deg, #1a1a1a 0%, #3d3d3d 100%); -webkit-background-clip: text; background-clip: text; color: transparent; } @media (prefers-color-scheme: dark) { .preview-page-title-text { background: linear-gradient(90deg, #f5f5f7 0%, #d1d1d6 100%); -webkit-background-clip: text; background-clip: text; color: transparent; } } .preview-main { flex: 1 1 auto; min-height: 0; width: min(1080px, 100vw); max-width: 100%; margin: 0 auto; padding: clamp(8px, 1.2vh, 12px) clamp(12px, 2vw, 16px) clamp(10px, 1.5vh, 16px); display: flex; flex-direction: column; gap: clamp(8px, 1.2vh, 12px); overflow: hidden; } .note-detail-shell { display: flex; flex-wrap: wrap; align-items: stretch; gap: 0; flex: 1 1 auto; min-height: 0; background: var(--panel-bg); border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow-card); border: 1px solid rgba(0,0,0,0.04); margin-bottom: 0; transition: box-shadow 0.25s ease; } .note-detail-shell:hover { box-shadow: var(--shadow-card-hover); } .note-detail-shell--no-carousel { min-height: unset; } .note-detail-shell--meta-only .note-detail-meta-column { flex: 1 1 100%; max-width: 100%; } .note-detail-shell--carousel-only .note-detail-carousel-column { flex: 1 1 100%; max-width: 100%; } .note-detail-shell--empty { min-height: 0; margin-bottom: 12px; box-shadow: none; background: transparent; } .note-detail-carousel-column { flex: 1 1 340px; min-width: min(100%, 340px); min-height: 0; background: linear-gradient(180deg, #121212 0%, #0a0a0a 100%); display: flex; flex-direction: column; } .carousel-stage { position: relative; flex: 1; display: flex; flex-direction: column; min-height: 0; } .carousel-scroll-viewport { flex: 1; overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scrollbar-width: none; -ms-overflow-style: none; outline: none; touch-action: pan-x; } .carousel-scroll-viewport::-webkit-scrollbar { display: none; width: 0; height: 0; } .carousel-scroll-viewport:focus-visible { box-shadow: inset 0 0 0 2px var(--xhs-red); } .carousel-track { display: flex; flex-direction: row; height: 100%; min-height: 0; } .carousel-counter-pill { position: absolute; top: 12px; right: 12px; z-index: 2; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-variant-numeric: tabular-nums; color: #fff; background: rgba(0, 0, 0, 0.45); backdrop-filter: blur(6px); pointer-events: none; line-height: 1.35; } .carousel-dots { position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%); z-index: 2; display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 7px; max-width: calc(100% - 24px); pointer-events: auto; } .carousel-dot { width: 6px; height: 6px; padding: 0; border: none; border-radius: 50%; background: rgba(255, 255, 255, 0.38); cursor: pointer; flex-shrink: 0; transition: transform 0.2s ease, background 0.2s ease; } .carousel-dot:hover { background: rgba(255, 255, 255, 0.65); } .carousel-dot.is-active { background: #fff; transform: scale(1.2); box-shadow: 0 0 0 1px rgba(0,0,0,0.15); } .carousel-slide { flex: 0 0 100%; scroll-snap-align: start; scroll-snap-stop: always; display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 8px; min-height: 100%; align-self: stretch; } .carousel-slide img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; vertical-align: middle; border-radius: 4px; } .note-detail-meta-column { flex: 1 1 300px; min-width: min(100%, 280px); max-width: 100%; min-height: 0; box-sizing: border-box; overflow-y: auto; scrollbar-gutter: stable; } .meta-card { padding: clamp(16px, 2.2vh, 24px) clamp(16px, 2.5vw, 24px) clamp(18px, 2.5vh, 28px); } .meta-author-row { margin-bottom: 12px; } .meta-author-name { font-size: 15px; font-weight: 600; color: var(--text); } .meta-detail-title { font-size: clamp(17px, 2.5vw, 20px); font-weight: 600; margin: 0 0 16px; line-height: 1.45; letter-spacing: 0.02em; } .meta-stats-row { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 22px; margin-bottom: 18px; padding-bottom: 18px; border-bottom: 1px solid rgba(0,0,0,0.07); font-size: 13px; color: var(--muted); } .meta-stat-item { display: inline-flex; align-items: baseline; gap: 5px; } .meta-stat-label { opacity: 0.72; font-size: 12px; } .meta-stat-value { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; } .note-detail-plain-body { white-space: pre-wrap; word-break: break-word; line-height: 1.75; font-size: 15px; color: rgba(0,0,0,0.88); } .hashtag-row { margin-top: 14px; line-height: 1.9; } a.hashtag { color: #13386c; text-decoration: none; margin-right: 8px; } a.hashtag:hover { text-decoration: underline; } .emoji-row { margin-top: 8px; } img.inline-emoji { height: 1.15em; vertical-align: -0.2em; } @media (prefers-color-scheme: dark) { :root { --panel-bg: #2c2c2e; --page-bg-top: #1c1c1f; --page-bg-bottom: #121214; --text: #f2f2f7; --muted: rgba(255,255,255,0.48); --shadow-card: 0 4px 28px rgba(0,0,0,0.35); --shadow-card-hover: 0 8px 36px rgba(0,0,0,0.45); } body { background: linear-gradient(168deg, var(--page-bg-top) 0%, var(--page-bg-bottom) 100%); } a.hashtag { color: #8ab4ff; } .meta-stats-row { border-bottom-color: rgba(255,255,255,0.1); } .note-detail-plain-body { color: rgba(255,255,255,0.88); } .note-detail-shell { border-color: rgba(255,255,255,0.06); } } @media (max-width: 720px) { .preview-page-header { padding: clamp(10px, 2.5vh, 20px) clamp(12px, 4vw, 16px) 4px; } .note-detail-shell { flex-direction: column; } .note-detail-carousel-column { min-height: min(42vh, 40%); } .carousel-counter-pill { top: 8px; right: 8px; font-size: 11px; padding: 3px 8px; } .carousel-dots { bottom: 10px; gap: 6px; } } """ carousel_script = """ (function () { var carouselAutoAdvanceMilliseconds = 4000; function carouselViewportCurrentSlideZeroBasedIndex(carouselViewportElement, slideCount) { var w = carouselViewportElement.clientWidth; if (w <= 0) return 0; var i = Math.round(carouselViewportElement.scrollLeft / w); if (i < 0) i = 0; if (i >= slideCount) i = slideCount - 1; return i; } function carouselViewportScrollToSlideZeroBasedIndex(carouselViewportElement, slideZeroBasedIndex, slideCount) { var w = carouselViewportElement.clientWidth; if (w <= 0) return; if (slideZeroBasedIndex < 0) slideZeroBasedIndex = 0; if (slideZeroBasedIndex >= slideCount) slideZeroBasedIndex = slideCount - 1; carouselViewportElement.scrollTo({ left: slideZeroBasedIndex * w, behavior: "smooth" }); } function syncCarouselDotsAndCounterFromScroll(carouselViewportElement) { var stage = carouselViewportElement.closest(".carousel-stage"); var track = carouselViewportElement.querySelector(".carousel-track"); var slides = track ? track.querySelectorAll(".carousel-slide") : []; var slideCount = slides.length; if (!stage || !slideCount) return; var counter = stage.querySelector("[data-carousel-counter]"); var dotNodeList = stage.querySelectorAll("[data-carousel-dot-index]"); var activeIndex = carouselViewportCurrentSlideZeroBasedIndex(carouselViewportElement, slideCount); if (counter) { counter.textContent = (activeIndex + 1) + "/" + slideCount; } dotNodeList.forEach(function (dotElement) { var dotIndex = parseInt(dotElement.getAttribute("data-carousel-dot-index"), 10); if (dotIndex === activeIndex) { dotElement.classList.add("is-active"); } else { dotElement.classList.remove("is-active"); } }); } function clearCarouselAutoAdvanceTimer(carouselViewportElement) { var timerId = carouselViewportElement.__carouselAutoAdvanceIntervalId; if (timerId) { clearInterval(timerId); carouselViewportElement.__carouselAutoAdvanceIntervalId = null; } } function startCarouselAutoAdvanceIfMultipleSlides(carouselViewportElement) { clearCarouselAutoAdvanceTimer(carouselViewportElement); var track = carouselViewportElement.querySelector(".carousel-track"); var slides = track ? track.querySelectorAll(".carousel-slide") : []; if (slides.length <= 1) return; carouselViewportElement.__carouselAutoAdvanceIntervalId = setInterval(function () { var slideCount = slides.length; var w = carouselViewportElement.clientWidth; if (w <= 0) return; var currentIndex = carouselViewportCurrentSlideZeroBasedIndex(carouselViewportElement, slideCount); var nextIndex = (currentIndex + 1) % slideCount; carouselViewportElement.scrollTo({ left: nextIndex * w, behavior: "smooth" }); }, carouselAutoAdvanceMilliseconds); } function bindCarouselViewport(carouselViewportElement) { var stage = carouselViewportElement.closest(".carousel-stage"); var track = carouselViewportElement.querySelector(".carousel-track"); var slides = track ? track.querySelectorAll(".carousel-slide") : []; var slideCount = slides.length; carouselViewportElement.addEventListener("scroll", function () { syncCarouselDotsAndCounterFromScroll(carouselViewportElement); }); carouselViewportElement.addEventListener("wheel", function (wheelEvent) { if (Math.abs(wheelEvent.deltaY) <= Math.abs(wheelEvent.deltaX)) return; wheelEvent.preventDefault(); carouselViewportElement.scrollLeft += wheelEvent.deltaY; }, { passive: false }); if (stage) { stage.addEventListener("mouseenter", function () { clearCarouselAutoAdvanceTimer(carouselViewportElement); }); stage.addEventListener("mouseleave", function () { startCarouselAutoAdvanceIfMultipleSlides(carouselViewportElement); }); } if (stage) { stage.querySelectorAll("[data-carousel-dot-index]").forEach(function (dotElement) { dotElement.addEventListener("click", function () { var targetIndex = parseInt(dotElement.getAttribute("data-carousel-dot-index"), 10); carouselViewportScrollToSlideZeroBasedIndex(carouselViewportElement, targetIndex, slideCount); clearCarouselAutoAdvanceTimer(carouselViewportElement); startCarouselAutoAdvanceIfMultipleSlides(carouselViewportElement); }); }); } syncCarouselDotsAndCounterFromScroll(carouselViewportElement); startCarouselAutoAdvanceIfMultipleSlides(carouselViewportElement); } document.querySelectorAll("[data-carousel-viewport]").forEach(bindCarouselViewport); window.addEventListener("resize", function () { document.querySelectorAll("[data-carousel-viewport]").forEach(function (carouselViewportElement) { syncCarouselDotsAndCounterFromScroll(carouselViewportElement); }); }); })(); """ return f""" {page_title_escaped}

{page_title_escaped}

{inner}
""" def _collect_note_detail_sequence_directory_path_list( only_sequence_name: str | None, ) -> list[Path]: if not _NOTE_DETAIL_OUTPUT_ROOT_DIRECTORY.is_dir(): return [] result: list[Path] = [] for child_path in sorted( _NOTE_DETAIL_OUTPUT_ROOT_DIRECTORY.iterdir(), key=lambda p: p.name, ): if not child_path.is_dir(): continue if only_sequence_name is not None and child_path.name != only_sequence_name: continue result.append(child_path) return result def _build_note_card_metadata_by_sequence_name_dictionary( sequence_name_list: list[str], ) -> dict[str, dict[str, object]]: result_dictionary: dict[str, dict[str, object]] = {} for sequence_name in sequence_name_list: note_output_sequence_directory_path = _NOTE_OUTPUT_ROOT_DIRECTORY / sequence_name result_dictionary[sequence_name] = _read_note_card_metadata_dictionary( note_output_sequence_directory_path, ) return result_dictionary def start(command_line_argument_strings: list[str] | None = None) -> int: arguments = ( command_line_argument_strings if command_line_argument_strings is not None else sys.argv[1:] ) only_sequence_name: str | None = arguments[0].strip() if arguments else None if only_sequence_name == "": only_sequence_name = None note_detail_sequence_directory_path_list = _collect_note_detail_sequence_directory_path_list( only_sequence_name, ) sequence_name_list = [p.name for p in note_detail_sequence_directory_path_list] note_card_metadata_by_sequence_name_dictionary = ( _build_note_card_metadata_by_sequence_name_dictionary(sequence_name_list) ) html_document_string = _build_full_html_document_string( note_detail_sequence_directory_path_list, note_card_metadata_by_sequence_name_dictionary, ) _PREVIEW_HTML_OUTPUT_FILE_PATH.parent.mkdir(parents=True, exist_ok=True) _PREVIEW_HTML_OUTPUT_FILE_PATH.write_text( html_document_string, encoding="utf-8", ) preview_html_file_uri_string = _PREVIEW_HTML_OUTPUT_FILE_PATH.resolve().as_uri() webbrowser.open(preview_html_file_uri_string) return 0 def main() -> int: return start() __all__ = ["start", "main", "_NOTE_DETAIL_OUTPUT_ROOT_DIRECTORY"] if __name__ == "__main__": raise SystemExit(main())