inital commit

This commit is contained in:
Rekushi authored and GitHub committed 2026-10-25 22:54:32 +00:00
commit 8e155b599b
15 files changed
+711

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
"""Read and write frame-timed Flipnote Subtitle files. No dependencies."""
import re
MAX_FILE_BYTES = 1024 * 1024
MAX_PAGE = 2**53 - 1
class FsrtError(ValueError):
pass
def _normalise(cues, max_page, max_cues):
if type(max_page) is not int or not 1 <= max_page <= MAX_PAGE or type(max_cues) is not int or max_cues < 1:
raise FsrtError('Page and subtitle limits must be positive integers.')
if not isinstance(cues, list) or not cues:
raise FsrtError('The file has no subtitles.')
if len(cues) > max_cues:
raise FsrtError(f'Use no more than {max_cues} subtitles.')
result = []
for index, cue in enumerate(cues, 1):
cue = cue if isinstance(cue, dict) else {}
start, end, text = cue.get('start'), cue.get('end'), cue.get('text')
if type(start) is not int or type(end) is not int or not 1 <= start <= end <= min(max_page, MAX_PAGE):
raise FsrtError(f'Subtitle {index}: choose pages between 1 and {max_page}, with the end on or after the start.')
if not isinstance(text, str):
raise FsrtError(f'Subtitle {index}: enter some text.')
text = text.replace('\r\n', '\n').replace('\r', '\n')
if not text.strip() or re.search(r'[\x00-\x08\x0b-\x1f\x7f]', text) or any(not line.strip() for line in text.split('\n')):
raise FsrtError(f'Subtitle {index}: text must not contain blank lines or control characters.')
result.append({'start': start, 'end': end, 'text': text})
result.sort(key=lambda cue: (cue['start'], cue['end']))
if any(b['start'] <= a['end'] for a, b in zip(result, result[1:])):
raise FsrtError('Two subtitles overlap. End one before the next starts.')
return result
def _size_check(text):
try:
size = len(text.encode('utf-8'))
except UnicodeEncodeError:
raise FsrtError('Subtitle text must be valid UTF-8.') from None
if size > MAX_FILE_BYTES:
raise FsrtError('The subtitle file must be 1 MiB or smaller.')
def parse(text, *, max_page=MAX_PAGE, max_cues=10000):
if not isinstance(text, str):
raise FsrtError('Read the file as UTF-8 text.')
_size_check(text)
value = text.removeprefix('\ufeff').replace('\r\n', '\n').replace('\r', '\n')
value = re.sub(r'^(?:[ \t]*\n)+|(?:\n[ \t]*)+$', '', value)
if not value.strip():
raise FsrtError('The file has no subtitles.')
cues = []
for index, block in enumerate(re.split(r'\n(?:[ \t]*\n)+', value), 1):
lines = block.split('\n')
if not re.fullmatch(r'[ \t]*[0-9]{1,16}[ \t]*', lines[0]) or not 1 <= int(lines[0]) <= MAX_PAGE:
raise FsrtError(f'Subtitle {index}: put a positive sequence number on the first line.')
match = re.fullmatch(r'[ \t]*([0-9]{1,16})[ \t]*-->[ \t]*([0-9]{1,16})[ \t]*', lines[1] if len(lines) > 1 else '')
if not match:
raise FsrtError(f'Subtitle {index}: use page numbers such as 1 --> 12, instead of timestamps.')
cues.append({'start': int(match[1]), 'end': int(match[2]), 'text': '\n'.join(lines[2:])})
return _normalise(cues, max_page, max_cues)
def stringify(cues, *, max_page=MAX_PAGE, max_cues=10000):
values = _normalise(cues, max_page, max_cues)
text = '\n\n'.join(f"{index}\n{cue['start']} --> {cue['end']}\n{cue['text']}" for index, cue in enumerate(values, 1)) + '\n'
_size_check(text)
return text