File size: 5,581 Bytes
ac6c361
8edf447
ac6c361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8edf447
 
 
 
 
bf1a1f0
f3fb7bc
ac6c361
8edf447
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ac6c361
 
 
 
 
 
f3fb7bc
ac6c361
 
 
 
8edf447
 
 
 
 
ac6c361
f3fb7bc
8edf447
ac6c361
 
f3fb7bc
 
ac6c361
f3fb7bc
 
 
 
 
bf1a1f0
ac6c361
bf1a1f0
ac6c361
f3fb7bc
ac6c361
f3fb7bc
ac6c361
f3fb7bc
bf1a1f0
ac6c361
f3fb7bc
 
bf1a1f0
f3fb7bc
bf1a1f0
f3fb7bc
bf1a1f0
 
 
f3fb7bc
 
8edf447
 
 
ac6c361
bf1a1f0
f3fb7bc
ac6c361
f3fb7bc
8edf447
 
 
 
ac6c361
 
 
8edf447
 
bf1a1f0
 
 
8edf447
bf1a1f0
 
 
 
f3fb7bc
ac6c361
 
 
 
 
f3fb7bc
ac6c361
8edf447
 
 
ac6c361
040f97b
 
 
 
ac6c361
bf1a1f0
 
ac6c361
 
8edf447
040f97b
ac6c361
040f97b
 
8edf447
bf1a1f0
8edf447
bf1a1f0
8edf447
 
bf1a1f0
 
 
8edf447
bf1a1f0
ac6c361
 
 
f3fb7bc
ac6c361
f3fb7bc
ac6c361
bf1a1f0
ac6c361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
"""
subtitle_parser.py β€” Subtitle Parsing & Writing
Handles .srt / .vtt / .ass via pysubs2.
"""

import copy
import logging
import os
import re
import shutil
import tempfile

import pysubs2

logger = logging.getLogger(__name__)

WORKSPACE_DIR = os.path.join(tempfile.gettempdir(), "subsync_workspace")
os.makedirs(WORKSPACE_DIR, exist_ok=True)

# ASS styles that should NOT be translated (romaji, karaoke, etc.)
SKIP_STYLES = {
    "EDR", "Romaji", "JP", "Karaoke", "KAR",
    "Signs", "Lyrics_JP", "Romaji2", "OP_Romaji", "ED_Romaji",
}

_EXT_MAP = {".srt": "srt", ".vtt": "vtt", ".ass": "ass", ".ssa": "ass"}

_ASS_TAG_RE      = re.compile(r"\{[^}]*\}")
_HTML_TAG_RE     = re.compile(r"<[^>]+>")
_LEADING_TAGS_RE = re.compile(r"^(\{[^}]*\})+")


def is_skip_style(style_name: str) -> bool:
    """Return True if this ASS style should be skipped (not translated)."""
    if not style_name:
        return False
    s = style_name.strip()
    if s in SKIP_STYLES:
        return True
    sl = s.lower()
    return any(k in sl for k in ["romaji", "karaoke", "kar_", "_kar", "furigana"])


def _detect_format(filename: str) -> str:
    ext = os.path.splitext(filename.lower())[1]
    fmt = _EXT_MAP.get(ext)
    if not fmt:
        raise ValueError(
            f"'{ext}' format α€€α€­α€― support α€™α€œα€―α€•α€Ία€•α€« β€” .srt / .vtt / .ass α€žα€¬ α€œα€€α€Ία€α€Άα€žα€Šα€Ί"
        )
    return fmt


def _strip_tags(text: str) -> str:
    text = _ASS_TAG_RE.sub("", text)
    text = _HTML_TAG_RE.sub("", text)
    return text.strip()


def parse_subtitle(file_bytes: bytes, filename: str):
    """Parse subtitle bytes β†’ (SSAFile, fmt_string)."""
    fmt = _detect_format(filename)

    text = None
    for enc in ("utf-8-sig", "utf-8", "latin-1", "cp1252"):
        try:
            text = file_bytes.decode(enc)
            break
        except (UnicodeDecodeError, LookupError):
            continue
    if text is None:
        raise ValueError("ဖိုင် encoding ဖတ်၍ မရပါ")

    text = text.lstrip("\ufeff")

    try:
        subs = pysubs2.SSAFile.from_string(text)
        logger.info("Parsed %d events from '%s' (%s)", len(subs), filename, fmt)
        return subs, fmt
    except Exception as e1:
        logger.warning("from_string failed: %s", e1)

    try:
        suffix = os.path.splitext(filename)[1] or ("." + fmt)
        fd, tmp = tempfile.mkstemp(suffix=suffix)
        try:
            os.write(fd, file_bytes)
        finally:
            os.close(fd)
        subs = pysubs2.SSAFile.load(tmp)
        os.unlink(tmp)
        return subs, fmt
    except Exception as e2:
        raise ValueError(
            f"Parse မရပါ:\nAttempt 1: {e1}\nAttempt 2: {e2}"
        ) from e2


def get_plain_lines(ssafile) -> list:
    """
    Return list[str] same length as ssafile.events.
    - Comment events  β†’ "" (keep original, not displayed)
    - Skip styles     β†’ "" (EDR/romaji/karaoke etc.)
    - Normal events   β†’ clean text (tags stripped)
    Empty string β†’ replace_lines keeps original text unchanged.
    """
    lines = []
    for event in ssafile.events:
        # Skip ASS Comment lines (editor notes, not shown to viewer)
        if getattr(event, "is_comment", False):
            lines.append("")
            continue

        # Skip romaji / karaoke / non-English style tracks
        if is_skip_style(event.style):
            lines.append("")
            continue

        raw   = event.text.replace("\\N", "\n").replace("\\n", "\n")
        clean = _strip_tags(raw)
        lines.append(clean)
    return lines


def replace_lines(ssafile, new_lines: list):
    """
    Return new SSAFile with translated text. Timestamps never touched.
    Leading ASS override tags {\\pos(...)\\be5} preserved from original.
    Empty translation β†’ keep original event.text.
    """
    n_events = len(ssafile.events)
    n_lines  = len(new_lines)

    if n_lines != n_events:
        raise ValueError(
            f"TIMESTAMP SAFETY: translated lines ({n_lines}) "
            f"!= subtitle events ({n_events})"
        )

    result = copy.deepcopy(ssafile)

    for i, event in enumerate(result.events):
        translated = str(new_lines[i]).strip()

        # Empty β†’ keep original (skip-style or fallback)
        if not translated:
            continue

        # Re-prepend leading inline tags from original
        original_text = ssafile.events[i].text
        leading_m     = _LEADING_TAGS_RE.match(original_text)
        tag_prefix    = leading_m.group(0) if leading_m else ""

        event.text = tag_prefix + translated.replace("\n", "\\N")

    return result


def write_subtitle(ssafile, fmt: str) -> bytes:
    try:
        return ssafile.to_string(fmt).encode("utf-8")
    except Exception as exc:
        raise RuntimeError(f"Subtitle write မရပါ: {exc}") from exc


def output_filename(original_name: str) -> str:
    base, ext = os.path.splitext(original_name)
    return f"{base}.my{ext}"


def clean_workspace() -> None:
    try:
        if os.path.exists(WORKSPACE_DIR):
            shutil.rmtree(WORKSPACE_DIR)
        os.makedirs(WORKSPACE_DIR, exist_ok=True)
    except Exception as exc:
        raise RuntimeError(f"Workspace ရှင်းမရပါ: {exc}") from exc


def workspace_size_mb() -> float:
    total = 0
    for dirpath, _, filenames in os.walk(WORKSPACE_DIR):
        for f in filenames:
            try:
                total += os.path.getsize(os.path.join(dirpath, f))
            except OSError:
                pass
    return round(total / (1024 * 1024), 2)