#!/usr/bin/env python3
"""
解构产物 → 视频结构分析报告

从 deconstruction.json（video-parser 的输出）提取钩子、叙事结构、文案节奏、
场景编排等关键元素，输出结构化的分析报告 analysis.json。

用法:
  python analyze_video.py -i ./deconstructed_xxx/deconstruction.json
  python analyze_video.py -i ./deconstructed_xxx/ -o ./reports/
"""

import argparse
import json
import os
import re
import sys

QUIET = False


# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------

def log(msg: str) -> None:
    if not QUIET:
        print(msg, flush=True)


def log_err(msg: str) -> None:
    print(msg, file=sys.stderr, flush=True)


# ---------------------------------------------------------------------------
# Load
# ---------------------------------------------------------------------------

def load_deconstruction(input_path: str):
    """Load deconstruction.json, return (dict, input_dir)."""
    if os.path.isdir(input_path):
        json_path = os.path.join(input_path, "deconstruction.json")
    else:
        json_path = input_path

    if not os.path.isfile(json_path):
        log_err(f"找不到 deconstruction.json: {json_path}")
        sys.exit(1)

    with open(json_path, "r", encoding="utf-8") as f:
        data = json.load(f)

    input_dir = os.path.dirname(os.path.abspath(json_path))
    return data, input_dir


# ---------------------------------------------------------------------------
# Language detection
# ---------------------------------------------------------------------------

def detect_language(text: str) -> str:
    """Simple heuristic: if >30% CJK chars, treat as zh-CN, else en."""
    if not text:
        return "zh-CN"
    cjk_count = sum(1 for ch in text if '\u4e00' <= ch <= '\u9fff')
    return "zh-CN" if cjk_count > len(text) * 0.3 else "en"


# ---------------------------------------------------------------------------
# Utterance ↔ scene matching
# ---------------------------------------------------------------------------

def match_utterances_to_scenes(scenes: list, utterances: list) -> dict:
    """Assign ASR utterances to scenes by time overlap.

    Returns {scene_index: [utterance, ...]}
    """
    result = {s["index"]: [] for s in scenes}
    for utt in utterances:
        utt_start = utt.get("startTime", 0) / 1000.0
        utt_end = utt.get("endTime", 0) / 1000.0
        best_idx, best_overlap = None, 0.0
        for s in scenes:
            overlap = max(0, min(utt_end, s["endTimeSec"]) - max(utt_start, s["startTimeSec"]))
            if overlap > best_overlap:
                best_overlap = overlap
                best_idx = s["index"]
        if best_idx is not None:
            result[best_idx].append(utt)
    return result


# ---------------------------------------------------------------------------
# Scene roles
# ---------------------------------------------------------------------------

def infer_scene_roles(scene_count: int) -> list:
    """Infer role labels from scene count.

    Returns list like ["opening", "point", ..., "ending"].
    """
    if scene_count <= 0:
        return ["opening"]
    if scene_count == 1:
        return ["opening"]
    if scene_count == 2:
        return ["opening", "ending"]
    if scene_count == 3:
        return ["opening", "point", "ending"]
    roles = ["opening"]
    for _ in range(scene_count - 2):
        roles.append("point")
    roles.append("ending")
    return roles


# ---------------------------------------------------------------------------
# Hook extraction
# ---------------------------------------------------------------------------

def analyze_hook(scenes: list, utterance_map: dict, asr: dict) -> dict:
    """Extract hook from the first 3 seconds of the first scene."""
    if not scenes:
        return {"text": "", "durationSec": 0.0, "strategy": "none"}

    first_scene = scenes[0]
    hook_end = first_scene["startTimeSec"] + 3.0

    # Collect words that fall within the hook window.
    # Space tokens (startTime == -1) are included between valid words
    # to preserve natural text spacing.
    hook_words = []
    utterances = asr.get("utterances", [])
    exceeded = False
    for utt in utterances:
        if exceeded:
            break
        for w in utt.get("words", []):
            w_start = w.get("startTime", -1) / 1000.0
            if w_start < 0:
                # Space/punctuation token — include if we haven't exceeded the window
                hook_words.append(w["text"])
                continue
            if w_start >= hook_end:
                exceeded = True
                break
            hook_words.append(w["text"])

    hook_text = "".join(hook_words).strip()
    if not hook_text:
        # Fallback: use the first utterance text in the first scene
        first_utts = utterance_map.get(first_scene["index"], [])
        if first_utts:
            hook_text = first_utts[0].get("text", "")

    # Determine hook duration (end of last word in hook window)
    hook_duration = 3.0
    for utt in utterances:
        for w in utt.get("words", []):
            w_start = w.get("startTime", -1) / 1000.0
            w_end = w.get("endTime", -1) / 1000.0
            if 0 <= w_start < hook_end and w_end > 0:
                hook_duration = min(3.0, w_end)

    # Classify hook strategy
    strategy = "none"
    if hook_text:
        stripped = hook_text.strip()
        if stripped.endswith("?") or stripped.endswith("？"):
            strategy = "question"
        elif any(kw in stripped.lower() for kw in ["you", "your", "你", "你们", "想不想", "是不是"]):
            strategy = "question"
        else:
            strategy = "direct-content"

    # Word count: non-space characters for CJK, space-separated tokens for English
    hook_word_count = len(hook_text.replace(" ", "")) if any(
        '\u4e00' <= ch <= '\u9fff' for ch in hook_text
    ) else len(hook_text.split())

    return {
        "text": hook_text,
        "durationSec": round(hook_duration, 1),
        "strategy": strategy,
        "wordCount": hook_word_count,
    }


# ---------------------------------------------------------------------------
# Narrative structure
# ---------------------------------------------------------------------------

def build_narrative_structure(scenes: list, utterance_map: dict, total_duration: float) -> list:
    """Build per-scene narrative structure entries."""
    roles = infer_scene_roles(len(scenes))
    structure = []
    for i, scene in enumerate(scenes):
        duration = round(scene["endTimeSec"] - scene["startTimeSec"], 3)
        pct = round(duration / total_duration * 100, 1) if total_duration > 0 else 0.0

        utts = utterance_map.get(scene["index"], [])
        text = " ".join(u.get("text", "") for u in utts).strip()
        text_len = len(text.replace(" ", ""))
        density = round(text_len / duration, 1) if duration > 0 else 0.0

        structure.append({
            "sceneIndex": scene["index"],
            "role": roles[i] if i < len(roles) else "point",
            "durationSec": round(duration, 1),
            "durationPct": pct,
            "text": text,
            "textDensity": density,
            "keyframe": scene.get("keyframe", ""),
        })
    return structure


# ---------------------------------------------------------------------------
# Pacing analysis
# ---------------------------------------------------------------------------

def analyze_pacing(scenes: list, total_duration: float) -> dict:
    """Analyze scene pacing / rhythm."""
    durations = []
    for s in scenes:
        d = round(s["endTimeSec"] - s["startTimeSec"], 1)
        durations.append(d)

    if not durations:
        return {
            "avgSceneDuration": 0.0,
            "minSceneDuration": 0.0,
            "maxSceneDuration": 0.0,
            "rhythm": "uniform",
            "sceneDurations": [],
        }

    avg = round(sum(durations) / len(durations), 1)
    mn = min(durations)
    mx = max(durations)

    # Determine rhythm
    if len(durations) == 1:
        rhythm = "uniform"
    elif mx - mn < 1.0:
        rhythm = "uniform"
    else:
        first_half = sum(durations[:len(durations) // 2])
        second_half = sum(durations[len(durations) // 2:])
        if len(durations) == 2:
            ratio = durations[0] / durations[1] if durations[1] > 0 else 1
            if ratio > 2:
                rhythm = "front-heavy"
            elif ratio < 0.5:
                rhythm = "back-heavy"
            else:
                rhythm = "varied"
        elif first_half > second_half * 1.5:
            rhythm = "front-heavy"
        elif second_half > first_half * 1.5:
            rhythm = "back-heavy"
        else:
            cv = (mx - mn) / avg if avg > 0 else 0
            rhythm = "varied" if cv > 0.5 else "uniform"

    return {
        "avgSceneDuration": avg,
        "minSceneDuration": mn,
        "maxSceneDuration": mx,
        "rhythm": rhythm,
        "sceneDurations": durations,
    }


# ---------------------------------------------------------------------------
# Keywords & silence detection
# ---------------------------------------------------------------------------

def extract_keywords(words: list, full_text: str, language: str) -> list:
    """Extract meaningful keywords from ASR data.

    For English: use word-level tokens, filter stopwords and punctuation.
    For Chinese: split full text on punctuation into phrases, then extract
    multi-character content tokens (simple segmentation by punctuation boundaries).
    """
    if language.startswith("zh"):
        return _extract_keywords_zh(full_text)
    return _extract_keywords_en(words)


# Common Chinese stopwords (function words with little semantic value)
_ZH_STOPWORDS = {
    "的", "了", "在", "是", "我", "们", "有", "和", "就", "不", "人", "都",
    "一", "一个", "上", "也", "很", "到", "说", "要", "去", "你", "会", "着",
    "没有", "看", "好", "自己", "这", "他", "她", "它", "吗", "什么", "那",
    "没", "把", "那个", "这个", "啊", "呢", "吧", "哦", "嗯", "哈", "呀",
    "被", "从", "对", "而", "但", "但是", "如果", "因为", "所以", "或者",
    "还", "还是", "之", "与", "及", "等", "让", "给", "跟", "比", "为",
    "能", "可以", "这样", "那样", "其", "于", "以", "来", "出", "里",
}


def _extract_keywords_zh(full_text: str) -> list:
    """Extract Chinese keywords by splitting on punctuation and filtering."""
    # Split on common punctuation to get phrase segments
    segments = re.split(r'[,，。、！？!?.…:：;；\-—–\s\'\"()（）\[\]【】]+', full_text)

    keywords = []
    seen = set()
    for seg in segments:
        seg = seg.strip()
        if not seg:
            continue
        # Extract multi-char tokens from each segment using a simple greedy approach:
        # scan for runs of CJK characters (2+ chars) and non-CJK tokens (e.g. "1V1", "3500")
        tokens = re.findall(r'[\u4e00-\u9fff]{2,}|[A-Za-z0-9]+(?:[A-Za-z0-9]+)*', seg)
        for tok in tokens:
            lower = tok.lower()
            if lower in seen:
                continue
            if tok in _ZH_STOPWORDS:
                continue
            # Skip very short CJK tokens that are likely function words
            if len(tok) == 2 and all('\u4e00' <= c <= '\u9fff' for c in tok) and tok in _ZH_STOPWORDS:
                continue
            seen.add(lower)
            keywords.append(tok)
    return keywords


def _extract_keywords_en(words: list) -> list:
    """Extract English keywords from word-level ASR data."""
    stop_pattern = re.compile(r'^[\s,，。、！？!?.…:：;；\-—–\'\"()（）\[\]【】]+$')
    en_stopwords = {
        "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
        "have", "has", "had", "do", "does", "did", "will", "would", "could",
        "should", "may", "might", "shall", "can", "to", "of", "in", "for",
        "on", "with", "at", "by", "from", "as", "into", "through", "during",
        "before", "after", "and", "but", "or", "nor", "not", "so", "yet",
        "both", "either", "neither", "each", "every", "all", "any", "few",
        "more", "most", "other", "some", "such", "no", "only", "own", "same",
        "than", "too", "very", "just", "because", "if", "when", "where",
        "how", "what", "which", "who", "whom", "this", "that", "these",
        "those", "i", "me", "my", "we", "our", "you", "your", "he", "him",
        "his", "she", "her", "it", "its", "they", "them", "their",
    }
    keywords = []
    seen = set()
    for w in words:
        text = w.get("text", "").strip()
        if not text or stop_pattern.match(text):
            continue
        if w.get("startTime", -1) < 0:
            continue
        lower = text.lower()
        if lower in en_stopwords:
            continue
        if lower not in seen:
            seen.add(lower)
            keywords.append(text)
    return keywords


def detect_silence_segments(utterances: list, total_duration: float) -> list:
    """Find gaps between utterances longer than 1 second."""
    if not utterances:
        return []

    segments = []
    # Gap before first utterance
    first_start = utterances[0].get("startTime", 0) / 1000.0
    if first_start > 1.0:
        segments.append({
            "startSec": 0.0,
            "endSec": round(first_start, 1),
            "durationSec": round(first_start, 1),
        })

    # Gaps between utterances
    for i in range(len(utterances) - 1):
        cur_end = utterances[i].get("endTime", 0) / 1000.0
        next_start = utterances[i + 1].get("startTime", 0) / 1000.0
        gap = next_start - cur_end
        if gap > 1.0:
            segments.append({
                "startSec": round(cur_end, 1),
                "endSec": round(next_start, 1),
                "durationSec": round(gap, 1),
            })

    # Gap after last utterance
    last_end = utterances[-1].get("endTime", 0) / 1000.0
    if total_duration - last_end > 1.0:
        segments.append({
            "startSec": round(last_end, 1),
            "endSec": round(total_duration, 1),
            "durationSec": round(total_duration - last_end, 1),
        })

    return segments


# ---------------------------------------------------------------------------
# Text structure
# ---------------------------------------------------------------------------

def analyze_text_structure(asr: dict, scenes: list, utterance_map: dict, total_duration: float) -> dict:
    """Analyze text structure: speech rate, silence segments, keywords."""
    full_text = asr.get("text", "")
    utterances = asr.get("utterances", [])

    # Collect all words from all utterances
    all_words = []
    for utt in utterances:
        all_words.extend(utt.get("words", []))

    # Speech duration (sum of utterance durations)
    speech_duration = 0.0
    for utt in utterances:
        s = utt.get("startTime", 0) / 1000.0
        e = utt.get("endTime", 0) / 1000.0
        speech_duration += max(0, e - s)

    # Text length (non-space characters)
    text_len = len(full_text.replace(" ", ""))

    # Speech rate: chars per second of speech
    speech_rate = round(text_len / speech_duration, 1) if speech_duration > 0 else 0.0

    # Silence segments
    silence_segments = detect_silence_segments(utterances, total_duration)

    # Keywords
    keywords = extract_keywords(all_words, full_text, detect_language(full_text))

    return {
        "fullText": full_text,
        "speechRate": speech_rate,
        "silenceSegments": silence_segments,
        "keywords": keywords,
    }


# ---------------------------------------------------------------------------
# Audio pattern
# ---------------------------------------------------------------------------

def analyze_audio_pattern(asr: dict, audio: dict, total_duration: float) -> dict:
    """Analyze audio pattern: narration presence, speech/silence ratio."""
    utterances = asr.get("utterances", [])
    has_narration = len(utterances) > 0

    narration_url = audio.get("url", "")

    # Total speech duration
    speech_duration = 0.0
    for utt in utterances:
        s = utt.get("startTime", 0) / 1000.0
        e = utt.get("endTime", 0) / 1000.0
        speech_duration += max(0, e - s)

    speech_ratio = round(speech_duration / total_duration, 2) if total_duration > 0 else 0.0
    silence_ratio = round(1.0 - speech_ratio, 2)

    return {
        "hasNarration": has_narration,
        "narrationUrl": narration_url,
        "speechRatio": speech_ratio,
        "silenceRatio": silence_ratio,
    }


# ---------------------------------------------------------------------------
# CTA detection
# ---------------------------------------------------------------------------

CTA_KEYWORDS = [
    # English
    "subscribe", "like", "follow", "share", "comment", "click", "link",
    "sign up", "buy now", "check out", "tap", "swipe up", "join",
    # Chinese — verbs / phrases commonly used in CTA
    "关注", "点赞", "转发", "评论", "订阅", "分享", "点击", "链接",
    "扫码", "加入", "报名", "下单", "购买", "赶紧", "抓紧", "安排",
    "左下角", "右下角", "小黄车", "橱窗", "主页", "私信", "留言",
]


def analyze_cta(scenes: list, utterance_map: dict, utterances: list) -> dict:
    """Detect call-to-action in the ending portion of the video.

    When there are multiple scenes, check the last scene's text.
    When there is only one scene, check the last utterance(s) to avoid
    treating the entire video text as the ending.
    """
    if not scenes:
        return {"hasExplicitCta": False, "endingText": "", "endingDurationSec": 0.0}

    last_scene = scenes[-1]
    scene_duration = round(last_scene["endTimeSec"] - last_scene["startTimeSec"], 1)

    # Determine ending text: use last scene's utterances,
    # but if there's only one scene, narrow down to the last utterance(s)
    utts = utterance_map.get(last_scene["index"], [])
    if len(scenes) == 1 and len(utts) > 1:
        # Single-scene video: take the last 1-2 utterances as ending
        ending_utts = utts[-2:] if len(utts) >= 2 else utts[-1:]
        ending_text = " ".join(u.get("text", "") for u in ending_utts).strip()
        # Approximate ending duration from last utterance timestamps
        first_end_utt = ending_utts[0]
        ending_duration = round(
            (last_scene["endTimeSec"] - first_end_utt.get("startTime", 0) / 1000.0), 1
        )
    else:
        ending_text = " ".join(u.get("text", "") for u in utts).strip()
        ending_duration = scene_duration

    # Keyword matching
    lower_text = ending_text.lower()
    matched_count = sum(1 for kw in CTA_KEYWORDS if kw in lower_text)
    has_cta = matched_count > 0

    # Strength: none / weak / strong
    if matched_count == 0:
        strength = "none"
    elif matched_count == 1:
        strength = "weak"
    else:
        strength = "strong"

    return {
        "hasExplicitCta": has_cta,
        "endingText": ending_text,
        "endingDurationSec": ending_duration,
        "strength": strength,
    }


# ---------------------------------------------------------------------------
# Utterance metrics
# ---------------------------------------------------------------------------

def analyze_utterance_metrics(utterances: list) -> dict:
    """Compute per-utterance statistics: char counts, durations, density."""
    if not utterances:
        return {
            "count": 0,
            "avgCharCount": 0.0,
            "minCharCount": 0,
            "maxCharCount": 0,
            "avgDurationSec": 0.0,
            "minDurationSec": 0.0,
            "maxDurationSec": 0.0,
            "avgDensity": 0.0,
            "utterances": [],
        }

    items = []
    for idx, utt in enumerate(utterances):
        text = utt.get("text", "")
        char_count = len(text.replace(" ", ""))
        start = utt.get("startTime", 0) / 1000.0
        end = utt.get("endTime", 0) / 1000.0
        dur = max(0.0, end - start)
        density = round(char_count / dur, 1) if dur > 0 else 0.0
        items.append({
            "index": idx,
            "text": text,
            "durationSec": round(dur, 1),
            "charCount": char_count,
            "density": density,
        })

    char_counts = [it["charCount"] for it in items]
    durations = [it["durationSec"] for it in items]
    densities = [it["density"] for it in items]

    return {
        "count": len(items),
        "avgCharCount": round(sum(char_counts) / len(char_counts), 1),
        "minCharCount": min(char_counts),
        "maxCharCount": max(char_counts),
        "avgDurationSec": round(sum(durations) / len(durations), 1),
        "minDurationSec": min(durations),
        "maxDurationSec": max(durations),
        "avgDensity": round(sum(densities) / len(densities), 1),
        "utterances": items,
    }


# ---------------------------------------------------------------------------
# Speech rhythm
# ---------------------------------------------------------------------------

def analyze_speech_rhythm(utterances: list) -> dict:
    """Analyze word-level micro-rhythm: gaps, pauses, tempo profile."""
    # Collect all words with valid timestamps across all utterances
    all_words = []
    for utt in utterances:
        for w in utt.get("words", []):
            if w.get("startTime", -1) >= 0:
                all_words.append(w)

    if len(all_words) < 2:
        return {
            "avgWordGapMs": 0,
            "maxWordGapMs": 0,
            "intentionalPauses": [],
            "tempoProfile": "steady",
        }

    # Sort words by start time
    all_words.sort(key=lambda w: w["startTime"])

    gaps = []
    pauses = []
    for i in range(1, len(all_words)):
        prev_end = all_words[i - 1].get("endTime", 0)
        cur_start = all_words[i].get("startTime", 0)
        gap = cur_start - prev_end
        if gap < 0:
            continue
        gaps.append(gap)
        if gap > 500:
            pauses.append({
                "afterWord": all_words[i - 1].get("text", "").strip(),
                "gapMs": int(gap),
                "timestampSec": round(prev_end / 1000.0, 1),
            })

    avg_gap = int(sum(gaps) / len(gaps)) if gaps else 0
    max_gap = int(max(gaps)) if gaps else 0

    # Tempo profile: compare first-half vs second-half avg gap
    if len(gaps) < 4:
        tempo = "steady"
    else:
        mid = len(gaps) // 2
        first_avg = sum(gaps[:mid]) / mid
        second_avg = sum(gaps[mid:]) / (len(gaps) - mid)
        if first_avg > 0 and second_avg / first_avg < 0.7:
            tempo = "accelerating"
        elif second_avg > 0 and first_avg / second_avg < 0.7:
            tempo = "decelerating"
        elif max_gap > avg_gap * 3 and len(pauses) >= 2:
            tempo = "varied"
        else:
            tempo = "steady"

    return {
        "avgWordGapMs": avg_gap,
        "maxWordGapMs": max_gap,
        "intentionalPauses": pauses,
        "tempoProfile": tempo,
    }


# ---------------------------------------------------------------------------
# Repetition analysis
# ---------------------------------------------------------------------------

def analyze_repetition(full_text: str, words: list, language: str) -> dict:
    """Detect repeated words/phrases and compute repetition ratio."""
    from collections import Counter

    if language.startswith("zh"):
        # Chinese: tokenize via punctuation-split + CJK/alphanum extraction (no dedup)
        segments = re.split(r'[,，。、！？!?.…:：;；\-—–\s\'\"()（）\[\]【】]+', full_text)
        tokens = []
        for seg in segments:
            seg = seg.strip()
            if not seg:
                continue
            toks = re.findall(r'[\u4e00-\u9fff]{2,}|[A-Za-z0-9]+(?:[A-Za-z0-9]+)*', seg)
            for tok in toks:
                if tok not in _ZH_STOPWORDS:
                    tokens.append(tok)
    else:
        # English: use word-level tokens, filter stopwords
        en_stopwords = {
            "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
            "have", "has", "had", "do", "does", "did", "will", "would", "could",
            "should", "may", "might", "shall", "can", "to", "of", "in", "for",
            "on", "with", "at", "by", "from", "as", "into", "through", "during",
            "before", "after", "and", "but", "or", "nor", "not", "so", "yet",
            "both", "either", "neither", "each", "every", "all", "any", "few",
            "more", "most", "other", "some", "such", "no", "only", "own", "same",
            "than", "too", "very", "just", "because", "if", "when", "where",
            "how", "what", "which", "who", "whom", "this", "that", "these",
            "those", "i", "me", "my", "we", "our", "you", "your", "he", "him",
            "his", "she", "her", "it", "its", "they", "them", "their",
        }
        tokens = []
        for w in words:
            text = w.get("text", "").strip()
            if not text or w.get("startTime", -1) < 0:
                continue
            lower = text.lower()
            if lower in en_stopwords:
                continue
            if re.match(r'^[\s,，。、！？!?.…:：;；\-—–\'\"()（）\[\]【】]+$', text):
                continue
            tokens.append(lower)

    counter = Counter(tokens)
    top_repeated = [
        {"word": word, "count": cnt}
        for word, cnt in counter.most_common()
        if cnt >= 2
    ][:10]

    total_words = len(tokens)
    unique_words = len(counter)
    ratio = round(1 - unique_words / total_words, 2) if total_words > 0 else 0.0

    return {
        "topRepeated": top_repeated,
        "totalUniqueWords": unique_words,
        "totalWords": total_words,
        "repetitionRatio": ratio,
    }


# ---------------------------------------------------------------------------
# Sentence type classification
# ---------------------------------------------------------------------------

_IMPERATIVE_STARTERS_ZH = [
    "点", "看", "赶紧", "抓紧", "快", "记得", "一定", "别忘",
    "关注", "点赞", "转发", "评论", "订阅", "分享", "点击",
    "扫码", "加入", "报名", "下单", "购买", "安排", "留言",
]
_IMPERATIVE_STARTERS_EN = [
    "click", "tap", "subscribe", "like", "follow", "share", "comment",
    "check", "go", "try", "watch", "listen", "join", "sign", "buy",
    "get", "grab", "hit", "swipe", "make", "let", "don't forget",
    "remember",
]


def analyze_sentence_types(full_text: str, language: str) -> dict:
    """Classify sentences by type: declarative, interrogative, imperative, exclamatory."""
    # Split into sentences by sentence-ending punctuation
    sentences = re.split(r'[。！？!?.]', full_text)
    sentences = [s.strip() for s in sentences if s.strip()]

    if not sentences:
        return {
            "declarative": 0,
            "interrogative": 0,
            "imperative": 0,
            "exclamatory": 0,
            "distribution": {
                "declarative": 0.0,
                "interrogative": 0.0,
                "imperative": 0.0,
                "exclamatory": 0.0,
            },
        }

    # To classify we need the original ending punctuation, so re-scan full_text
    # Build list of (sentence_text, ending_punctuation)
    parts = re.findall(r'([^。！？!?.]+)([。！？!?.])', full_text)
    # If regex didn't capture all, fallback to simple classification
    if not parts:
        parts = [(s, "。") for s in sentences]

    counts = {"declarative": 0, "interrogative": 0, "imperative": 0, "exclamatory": 0}

    imp_starters = _IMPERATIVE_STARTERS_ZH if language.startswith("zh") else _IMPERATIVE_STARTERS_EN

    for text, punct in parts:
        text = text.strip()
        if not text:
            continue
        if punct in ("？", "?"):
            counts["interrogative"] += 1
        elif punct in ("！", "!"):
            counts["exclamatory"] += 1
        elif any(text.lower().startswith(kw) for kw in imp_starters):
            counts["imperative"] += 1
        else:
            counts["declarative"] += 1

    total = sum(counts.values())
    dist = {}
    for k, v in counts.items():
        dist[k] = round(v / total, 3) if total > 0 else 0.0

    return {
        **counts,
        "distribution": dist,
    }


# ---------------------------------------------------------------------------
# Assets summary
# ---------------------------------------------------------------------------

def build_assets_summary(decon: dict) -> dict:
    """Build assets summary from deconstruction data."""
    audio = decon.get("audio", {})
    keyframes = decon.get("keyframes", [])

    audio_info = {}
    if audio.get("localPath") or audio.get("url"):
        audio_info = {
            "localPath": audio.get("localPath", ""),
            "url": audio.get("url", ""),
        }

    kf_list = []
    for kf in keyframes:
        kf_list.append({
            "index": kf["index"],
            "localPath": kf.get("localPath", ""),
            "timestampSec": kf.get("timestampSec", 0.0),
        })

    result = {}
    if audio_info:
        result["audio"] = audio_info
    if kf_list:
        result["keyframes"] = kf_list
    return result


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="Generate a video structural analysis report (analysis.json) from a deconstruction artifact",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python analyze_video.py -i ./deconstructed_xxx/deconstruction.json
  python analyze_video.py -i ./deconstructed_xxx/ -o ./reports/
  python analyze_video.py -i ./deconstructed_xxx/ --json-output
        """,
    )

    parser.add_argument(
        "-i", "--input",
        required=True,
        help="Path to deconstruction.json or its parent directory",
    )
    parser.add_argument(
        "-o", "--output-dir",
        default=None,
        help="Output directory (default: same as input)",
    )
    parser.add_argument(
        "--json-output",
        action="store_true",
        default=False,
        help="Only print JSON to stdout (pipeline mode)",
    )

    args = parser.parse_args()

    global QUIET
    QUIET = args.json_output

    # ------------------------------------------------------------------
    # [1] Load deconstruction
    # ------------------------------------------------------------------
    log("[1/13] Loading deconstruction data...")
    decon, input_dir = load_deconstruction(args.input)
    log(f"   loaded: {input_dir}/deconstruction.json")

    output_dir = args.output_dir or input_dir
    os.makedirs(output_dir, exist_ok=True)

    # ------------------------------------------------------------------
    # [2] Basic info
    # ------------------------------------------------------------------
    log("[2/13] Basic analysis...")
    source = decon.get("source", {})
    duration_ms = source.get("durationMs", 0)
    duration_sec = duration_ms / 1000.0

    scenes = decon.get("scenes", [])
    asr = decon.get("asr", {})
    audio = decon.get("audio", {})
    utterances = asr.get("utterances", [])
    full_text = asr.get("text", "")
    keyframes = decon.get("keyframes", [])

    language = detect_language(full_text)
    asset_types = []
    if keyframes:
        asset_types.append("image")
    if audio.get("url") or audio.get("localPath"):
        asset_types.append("audio")

    source_info = {
        "url": source.get("url", ""),
        "durationMs": duration_ms,
        "durationSec": round(duration_sec, 1),
    }

    overview = {
        "language": language,
        "sceneCount": len(scenes),
        "totalTextLength": len(full_text.replace(" ", "")),
        "assetTypes": asset_types,
    }

    log(f"   duration: {duration_sec:.1f}s, scenes: {len(scenes)}, language: {language}")

    # ------------------------------------------------------------------
    # [3] Scene structure (utterance mapping)
    # ------------------------------------------------------------------
    log("[3/13] Scene-structure analysis...")
    utterance_map = match_utterances_to_scenes(scenes, utterances) if scenes and utterances else {}
    narrative = build_narrative_structure(scenes, utterance_map, duration_sec)
    for ns in narrative:
        log(f"   scene {ns['sceneIndex']}: {ns['role']} ({ns['durationSec']}s, {ns['durationPct']}%)")

    # ------------------------------------------------------------------
    # [4] Hook extraction
    # ------------------------------------------------------------------
    log("[4/13] Hook extraction...")
    hook = analyze_hook(scenes, utterance_map, asr)
    log(f"   strategy: {hook['strategy']}, word count: {hook['wordCount']}, text: {hook['text'][:40]}...")

    # ------------------------------------------------------------------
    # [5] Pacing analysis
    # ------------------------------------------------------------------
    log("[5/13] Pacing analysis...")
    pacing = analyze_pacing(scenes, duration_sec)
    log(f"   rhythm: {pacing['rhythm']}, scene durations: {pacing['sceneDurations']}")

    # ------------------------------------------------------------------
    # [6] Text structure
    # ------------------------------------------------------------------
    log("[6/13] Copy-structure analysis...")
    text_structure = analyze_text_structure(asr, scenes, utterance_map, duration_sec)
    log(f"   speech rate: {text_structure['speechRate']} chars/sec, "
        f"关键词: {len(text_structure['keywords'])} 个, "
        f"静默段: {len(text_structure['silenceSegments'])} 处")

    # ------------------------------------------------------------------
    # [7] Audio pattern
    # ------------------------------------------------------------------
    log("[7/13] Audio-pattern analysis...")
    audio_pattern = analyze_audio_pattern(asr, audio, duration_sec)
    log(f"   speech ratio: {audio_pattern['speechRatio']}, "
        f"静默比例: {audio_pattern['silenceRatio']}")

    # ------------------------------------------------------------------
    # [8] CTA detection
    # ------------------------------------------------------------------
    log("[8/13] CTA detection...")
    cta = analyze_cta(scenes, utterance_map, utterances)
    log(f"   explicit CTA: {cta['hasExplicitCta']}, "
        f"强度: {cta['strength']}, "
        f"结尾文案: {cta['endingText'][:30]}")

    # ------------------------------------------------------------------
    # [9] Utterance metrics
    # ------------------------------------------------------------------
    log("[9/13] Sentence statistics...")
    utterance_metrics = analyze_utterance_metrics(utterances)
    log(f"   sentences: {utterance_metrics['count']}, "
        f"平均字数: {utterance_metrics['avgCharCount']}, "
        f"平均时长: {utterance_metrics['avgDurationSec']}s")

    # ------------------------------------------------------------------
    # [10] Speech rhythm
    # ------------------------------------------------------------------
    log("[10/13] Speech rhythm...")
    speech_rhythm = analyze_speech_rhythm(utterances)
    log(f"   avg word gap: {speech_rhythm['avgWordGapMs']}ms, "
        f"有意停顿: {len(speech_rhythm['intentionalPauses'])} 处, "
        f"节奏: {speech_rhythm['tempoProfile']}")

    # ------------------------------------------------------------------
    # [11] Repetition
    # ------------------------------------------------------------------
    log("[11/13] Repetition patterns...")
    all_words = []
    for utt in utterances:
        all_words.extend(utt.get("words", []))
    repetition = analyze_repetition(full_text, all_words, language)
    top3 = ", ".join(f"{r['word']}({r['count']})" for r in repetition["topRepeated"][:3])
    log(f"   repetition ratio: {repetition['repetitionRatio']}, "
        f"高频词: {top3 or '无'}")

    # ------------------------------------------------------------------
    # [12] Sentence types
    # ------------------------------------------------------------------
    log("[12/13] Sentence-type distribution...")
    sentence_types = analyze_sentence_types(full_text, language)
    log(f"   declarative: {sentence_types['declarative']}, "
        f"疑问: {sentence_types['interrogative']}, "
        f"祈使: {sentence_types['imperative']}, "
        f"感叹: {sentence_types['exclamatory']}")

    # ------------------------------------------------------------------
    # [13] Output
    # ------------------------------------------------------------------
    log("[13/13] Writing analysis report...")
    assets = build_assets_summary(decon)

    analysis = {
        "source": source_info,
        "overview": overview,
        "hook": hook,
        "narrativeStructure": narrative,
        "pacing": pacing,
        "textStructure": text_structure,
        "audioPattern": audio_pattern,
        "cta": cta,
        "utteranceMetrics": utterance_metrics,
        "speechRhythm": speech_rhythm,
        "repetition": repetition,
        "sentenceTypes": sentence_types,
        "assets": assets,
    }

    if args.json_output:
        print(json.dumps(analysis, ensure_ascii=False, indent=2))
    else:
        analysis_path = os.path.join(output_dir, "analysis.json")
        with open(analysis_path, "w", encoding="utf-8") as f:
            json.dump(analysis, f, ensure_ascii=False, indent=2)
        log(f"\nAnalysis complete!")
        log(f"  report: {analysis_path}")
        log(f"  duration: {source_info['durationSec']}s")
        log(f"  scenes: {overview['sceneCount']}")
        log(f"  rhythm: {pacing['rhythm']}")
        log(f"  hook: {hook['strategy']}")


if __name__ == "__main__":
    main()
