File size: 5,969 Bytes
96d97a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import re

_INLINE_TAG_RE = re.compile(r"\[([\d\.]+)(?:[:\-]([\d\.]+))?\]")
_DIGIT_RANGE_TAIL_RE = re.compile(r"([\d]+(?:\.\d+)?)\s*[-\u2013]\s*([\d]+(?:\.\d+)?)\s*$")

def _try_parse_num(s):
    """Parse s as integer. Tries digit conversion first, then word2number if available."""
    s = s.strip()
    try:
        return int(float(s))
    except (ValueError, TypeError):
        pass
    try:
        from word2number import w2n
        return int(w2n.word_to_num(s))
    except Exception:
        pass
    return None

def _parse_header(line):
    """Return (start, end_or_None) if line is a block segment header, else None.



    Valid format: one or more prefix words, followed by a number (digit or word form),

    optional range, then colon at end of line. Matching is case-insensitive.



    Examples:

        'Scene 1:'          -> (1, None)

        'My Scene 3:'       -> (3, None)

        'Shot 2-4:'         -> (2, 4)

        'Part eleven:'      -> (11, None)    [requires word2number]

        'segment twenty:'   -> (20, None)   [requires word2number]

    """
    line = line.strip()
    if not line.endswith(':'):
        return None
    body = line[:-1].rstrip()
    tokens = body.split()
    # Need at least 2 tokens: at least one prefix word + one number token
    if len(tokens) < 2:
        return None
    # Try digit range at end: "Scene 2-4:"
    m = _DIGIT_RANGE_TAIL_RE.search(body)
    if m and body[:m.start()].strip():
        start = _try_parse_num(m.group(1))
        end = _try_parse_num(m.group(2))
        if start is not None and end is not None:
            return (start, end)
    # Try 1..N tail tokens as word-or-digit number (longest candidate first)
    # Keep at least one prefix token so bare "1:" is not matched as a header
    max_num_tokens = min(4, len(tokens) - 1)
    for n in range(max_num_tokens, 0, -1):
        candidate = " ".join(tokens[-n:])
        val = _try_parse_num(candidate)
        if val is not None:
            return (val, None)
    return None

def _extract_inline_tag(text):
    """Extract first [n] or [n-m] weight tag from text.

    Returns (clean_text, weight_or_None). Tag is stripped from text.

    """
    m = _INLINE_TAG_RE.search(text)
    if not m:
        return text.strip(), None
    val1 = float(m.group(1))
    val2 = float(m.group(2)) if m.group(2) else None
    weight = (val2 - val1) if val2 is not None else val1
    clean = _INLINE_TAG_RE.sub("", text).strip()
    return clean, weight

def _parse_inline_syntax(text):
    """Parse pipe-separated inline syntax with optional [n-m] weight tags.



    Syntax examples:

        'one | two | three'                       -> equal weights

        'one [0-50] | two [50-150] | three [150]' -> proportional weights

    """
    segments = []
    for part in text.split('|'):
        clean, weight = _extract_inline_tag(part)
        if clean:
            segments.append({"text": clean, "weight": weight if weight is not None else 1.0})
    return segments

def _parse_block_syntax(text):
    """Parse block header syntax where each segment is preceded by a header line.



    Header format: any words followed by a number (or word-number) and a colon

    on its own line. Optional [n-m] inline tag in body overrides header weight.



    Syntax examples:

        'Scene 1:\\ntext\\nScene 2:\\ntext'

        'My Part 3-6:\\ntext'          -> weight = 6-3 = 3

        'segment eleven:\\ntext'       -> weight = 1.0 (single number = sequence marker)

    """
    lines = text.splitlines(keepends=True)
    raw_segments = []
    current_header = None
    current_body = []
    for line in lines:
        h = _parse_header(line)
        if h is not None:
            if current_body or current_header is not None:
                raw_segments.append((current_header, "".join(current_body)))
            current_header = h
            current_body = []
        else:
            current_body.append(line)
    if current_body or current_header is not None:
        raw_segments.append((current_header, "".join(current_body)))
    segments = []
    for header, body in raw_segments:
        clean, inline_weight = _extract_inline_tag(body)
        if not clean:
            continue
        if inline_weight is not None:
            weight = inline_weight
        elif header is not None:
            start, end = header
            # Range header: weight is proportional span. Single number: equal weight.
            weight = (end - start) if end is not None else 1.0
        else:
            weight = 1.0
        segments.append({"text": clean, "weight": weight})
    return segments

def parse_smart_prompt(text):
    """Parse smart_prompt text into a list of {"text": str, "weight": float} dicts.



    Detects syntax automatically:



    --- Inline (newline-agnostic) ---

    Segments separated by | with optional [n-m] proportional weight tags.

        'man walks | man runs | man jumps'

        'man walks [0-50] | man runs [50-150] | man jumps [150-200]'



    --- Block (newline-specific) ---

    Segments preceded by a header line: any words + number + colon on its own line.

    The number (or range) is stripped and used only for weight. Ordinal numbers

    like 'Scene 1:' are sequence markers (equal weight). Ranges like 'Scene 1-3:'

    assign proportional weight (3-1=2). Word-form numbers require word2number package.

        'Scene 1:\\nman walks\\nScene 2:\\nman runs'

        'My Shot 1-3:\\nman walks\\nMy Shot 3-7:\\nman runs'

        'segment eleven:\\nman walks'   (requires: pip install word2number)



    All syntax markers are fully stripped before text is returned.

    """
    lines = text.splitlines()
    has_blocks = any(_parse_header(line) is not None for line in lines)
    if has_blocks:
        return _parse_block_syntax(text)
    return _parse_inline_syntax(text)