자막 작업

This commit is contained in:
toki 2023-04-01 22:02:59 +09:00
parent 5e5619e8d8
commit f1d5c3ad50
11 changed files with 3182 additions and 238 deletions

View file

@ -1,7 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:dart_framework/log/log.dart';
import 'package:dart_framework/subtitle/ParserSRT.dart';
import 'package:dart_framework/subtitle/parser_smi.dart';
import 'package:dart_framework/subtitle/parser_srt.dart';
import 'package:path/path.dart' as path;
import 'package:dart_framework/core/application.dart';
@ -10,88 +11,35 @@ import 'package:dart_framework/charset.dart';
void main() async {
Application(() async {
var filePath = Platform.script.toFilePath(windows: true);
var file = File('${path.dirname(path.dirname(filePath))}/test_euckr.smi');
var file = File('${path.dirname(path.dirname(filePath))}/test_utf16le.smi');
var bytes = file.readAsBytesSync();
var encoding = Charset.detect(bytes, defaultEncoding: Utf8Codec());
var encoding = Charset.detect(bytes);
print(encoding);
var result = eucKr.decode(bytes);
// print(result);
//SRT
// var parser = ParserSRT();
// var subtitles = parser.getSubtitlesData(result!, SubtitleType.srt);
// var index = 0;
// for (var item in subtitles.subtitles) {
// print('$index. ${item.text}');
// index++;
// }
var result = encoding!.decode(bytes);
var ext = path.extension(file.path).toLowerCase();
if (ext == '.smi') {
//SMI
var smi = ParserSMI();
smi.getSubtitlesData(result!);
// var regExp = RegExp(
// r'<SYNC Start=(\d+)><P Class=KRCC>',
// caseSensitive: false,
// multiLine: true,
// );
var parser = ParserSMI();
var subtitles = parser.getSubtitlesData(result);
var index = 0;
for (var item in subtitles.subtitles) {
print('$index. ${item.text}');
index++;
}
} else if (ext == '.srt') {
//SRT
var parser = ParserSRT();
var subtitles = parser.getSubtitlesData(result);
var index = 0;
for (var item in subtitles.subtitles) {
print('$index. ${item.text}');
index++;
}
}
}, (error, stack) {
print(error);
print(stack);
exit(0);
});
}
class ParserSMI {
Subtitles getSubtitlesData(String subtitlesContent) {
subtitlesContent = subtitlesContent.replaceAll(
RegExp('<font (.*?=)(.*?>)|</font>|<b>|</b>| |<i>|</i>',
caseSensitive: false, multiLine: false),
'');
var regExp = RegExp(
r'<SYNC Start=(\d+)><P Class=(.*?>)(\r|\n)*((((?!<SYNC|<\/BODY).*)(\r|\n))+)',
caseSensitive: false,
multiLine: true,
);
final matches = regExp.allMatches(subtitlesContent).toList();
final subtitleList = <Subtitle>[];
Subtitle? lastItem;
for (final regExpMatch in matches) {
var startTime = Time(
Duration(milliseconds: int.parse(regExpMatch.group(1).toString())));
var content = regExpMatch.group(4)!;
if (lastItem != null) {
lastItem.endTime.duration = startTime.before;
lastItem = null;
}
if (!content.contains('&nbsp;')) {
content = content
.replaceAll(RegExp(r'(\r\n|\r|\n)', caseSensitive: false), '')
.replaceAll(RegExp(r'(<br>|<\/br>)', caseSensitive: false), ' ')
.trim();
if (content.isNotEmpty) {
if (subtitleList.isNotEmpty &&
content.contains(subtitleList.last.text)) {
subtitleList.last.text = content;
lastItem = subtitleList.last;
} else {
var endTime = Time(Duration());
lastItem =
Subtitle(startTime: startTime, endTime: endTime, text: content);
subtitleList.add(lastItem);
}
}
}
}
for (var item in subtitleList) {
print(
'${item.startTime.toString()} --> ${item.endTime.toString()} >> ${item.text}');
}
return Subtitles(subtitles: subtitleList);
}
}

View file

@ -405,10 +405,11 @@ class Charset {
/// Default detect list
static final defaultDetectOrder = <Encoding>[
utf8,
ascii,
eucKr,
eucJp,
shiftJis,
eucKr,
gbk,
windows874,
latin1,

View file

@ -11,32 +11,23 @@ class EucJPDecoder extends Converter<List<int>, String> {
convert(input) {
List<int> result = [];
for (int i = 0; i < input.length; i++) {
final c1 = input[i];
final List<int>? codes;
var c1 = input[i];
if (c1 <= 0x7E) {
// ASCII Compatible
codes = EUC_TABLE[c1];
result.addAll(EUC_TABLE[c1] ?? []);
} else if (c1 == 0x8e) {
// Hiragana
final c2 = input[++i];
codes = EUC_TABLE[(c1 << 8) + c2];
var c2 = input[++i];
result.addAll(EUC_TABLE[(c1 << 8) + c2] ?? []);
} else if (c1 == 0x8f) {
// JIS X 0212
final c2 = input[++i];
final c3 = input[++i];
codes = EUC_TABLE[(c1 << 16) + (c2 << 8) + c3];
var c2 = input[++i];
var c3 = input[++i];
result.addAll(EUC_TABLE[(c1 << 16) + (c2 << 8) + c3] ?? []);
} else {
// JIS X 0208
final c2 = input[++i];
codes = EUC_TABLE[(c1 << 8) + c2];
}
if (codes == null) {
if (_allowMalformed) {
throw FormatException('Unfinished Euc-JP octet sequence', input, i);
}
result.add(unicodeReplacementCharacterRune);
} else {
result.addAll(codes);
var c2 = input[++i];
result.addAll(EUC_TABLE[(c1 << 8) + c2] ?? []);
}
}
return utf8.decode(result);
@ -46,10 +37,10 @@ class EucJPDecoder extends Converter<List<int>, String> {
class EucJPEncoder extends Converter<String, List<int>> {
const EucJPEncoder();
@override
List<int> convert(String input) {
List<int> convert(String s) {
List<int> result = [];
for (int i = 0; i < input.length; i++) {
var bytes = utf8.encode(input[i]);
for (int i = 0; i < s.length; i++) {
var bytes = utf8.encode(s[i]);
var value = 0;
for (var i = 0, length = bytes.length; i < length; i++) {

View file

@ -1,5 +1,3 @@
// ignore_for_file: constant_identifier_names
const EUC_TABLE = {
0x00: [0],
0x01: [1],
@ -715,6 +713,36 @@ const EUC_TABLE = {
0xa8be: [226, 148, 165],
0xa8bf: [226, 148, 184],
0xa8c0: [226, 149, 130],
0xada1: [226, 145, 160],
0xada2: [226, 145, 161],
0xada3: [226, 145, 162],
0xada4: [226, 145, 163],
0xada5: [226, 145, 164],
0xada6: [226, 145, 165],
0xada7: [226, 145, 166],
0xada8: [226, 145, 167],
0xada9: [226, 145, 168],
0xadaa: [226, 145, 169],
0xadab: [226, 145, 170],
0xadac: [226, 145, 171],
0xadad: [226, 145, 172],
0xadae: [226, 145, 173],
0xadaf: [226, 145, 174],
0xadb0: [226, 145, 175],
0xadb1: [226, 145, 176],
0xadb2: [226, 145, 177],
0xadb3: [226, 145, 178],
0xadb4: [226, 145, 179],
0xadb5: [226, 133, 160],
0xadb6: [226, 133, 161],
0xadb7: [226, 133, 162],
0xadb8: [226, 133, 163],
0xadb9: [226, 133, 164],
0xadba: [226, 133, 165],
0xadbb: [226, 133, 166],
0xadbc: [226, 133, 167],
0xadbd: [226, 133, 168],
0xadbe: [226, 133, 169],
0xb0a1: [228, 186, 156],
0xb0a2: [229, 148, 150],
0xb0a3: [229, 168, 131],

View file

@ -1,134 +0,0 @@
// ignore: file_names
class ParserSRT {
Subtitles getSubtitlesData(
String subtitlesContent,
SubtitleType subtitleType,
) {
RegExp regExp;
if (subtitleType == SubtitleType.webvtt) {
regExp = RegExp(
r'((\d{2}):(\d{2}):(\d{2})\.(\d+)) +--> +((\d{2}):(\d{2}):(\d{2})\.(\d{3})).*[\r\n]+\s*((?:(?!\r?\n\r?).)*(\r\n|\r|\n)(?:.*))',
caseSensitive: false,
multiLine: true,
);
} else if (subtitleType == SubtitleType.srt) {
regExp = RegExp(
r'((\d{2}):(\d{2}):(\d{2})\,(\d+)) +--> +((\d{2}):(\d{2}):(\d{2})\,(\d{3})).*[\r\n]+\s*((?:(?!\r?\n\r?).)*(\r\n|\r|\n)(?:.*))',
caseSensitive: false,
multiLine: true,
);
} else {
throw Exception('Incorrect subtitle type');
}
final matches = regExp.allMatches(subtitlesContent).toList();
final subtitleList = <Subtitle>[];
for (final regExpMatch in matches) {
final startTimeHours = int.parse(regExpMatch.group(2)!);
final startTimeMinutes = int.parse(regExpMatch.group(3)!);
final startTimeSeconds = int.parse(regExpMatch.group(4)!);
final startTimeMilliseconds = int.parse(regExpMatch.group(5)!);
final endTimeHours = int.parse(regExpMatch.group(7)!);
final endTimeMinutes = int.parse(regExpMatch.group(8)!);
final endTimeSeconds = int.parse(regExpMatch.group(9)!);
final endTimeMilliseconds = int.parse(regExpMatch.group(10)!);
final text = removeAllHtmlTags(regExpMatch.group(11)!);
final startTime = Time(Duration(
hours: startTimeHours,
minutes: startTimeMinutes,
seconds: startTimeSeconds,
milliseconds: startTimeMilliseconds,
));
final endTime = Time(Duration(
hours: endTimeHours,
minutes: endTimeMinutes,
seconds: endTimeSeconds,
milliseconds: endTimeMilliseconds,
));
subtitleList.add(
Subtitle(startTime: startTime, endTime: endTime, text: text.trim()),
);
}
return Subtitles(subtitles: subtitleList);
}
String removeAllHtmlTags(String htmlText) {
final exp = RegExp(
'(<[^>]*>)',
multiLine: true,
);
var newHtmlText = htmlText;
exp.allMatches(htmlText).toList().forEach(
(RegExpMatch regExpMatch) {
newHtmlText = regExpMatch.group(0) == '<br>'
? newHtmlText.replaceAll(regExpMatch.group(0)!, '\n')
: newHtmlText.replaceAll(regExpMatch.group(0)!, '');
},
);
return newHtmlText;
}
}
class Subtitles {
Subtitles({
required this.subtitles,
});
final List<Subtitle> subtitles;
}
class Subtitle {
Subtitle({
required this.startTime,
required this.endTime,
required this.text,
});
Time startTime;
Time endTime;
String text;
List<Object?> get props => [startTime, endTime, text];
}
enum SubtitleType {
webvtt,
srt,
}
class Time {
Time(Duration duration) {
this.duration = duration;
}
late Duration _duration;
Duration get duration => _duration;
set duration(Duration value) {
_duration = value;
var timeStr = duration.toString();
var timeArr = timeStr.substring(0, timeStr.lastIndexOf('.') + 4).split(':');
hour = int.parse(timeArr[0]);
minutes = int.parse(timeArr[1]);
var secArr = timeArr[2].split('.');
seconds = int.parse(secArr[0]);
milliseconds = int.parse(secArr[1]);
}
late int hour;
late int minutes;
late int seconds;
late int milliseconds;
Duration get before {
return Duration(milliseconds: duration.inMilliseconds - 1);
}
@override
String toString() {
return '${hour.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}.${milliseconds.toString().padLeft(3, '0')}';
}
}

View file

@ -0,0 +1,74 @@
abstract class ParserBase {
static final RegExp _htmlRegExp = RegExp(
'(<[^>]*>)',
multiLine: true,
);
RegExp get regExp;
Subtitles getSubtitlesData(String subtitlesContent);
String removeAllHtmlTags(String htmlText) {
var newHtmlText = htmlText;
_htmlRegExp.allMatches(htmlText).toList().forEach(
(RegExpMatch regExpMatch) {
newHtmlText = regExpMatch.group(0) == '<br>'
? newHtmlText.replaceAll(regExpMatch.group(0)!, '\n')
: newHtmlText.replaceAll(regExpMatch.group(0)!, '');
},
);
return newHtmlText;
}
}
class Subtitles {
Subtitles({
required this.subtitles,
});
final List<Subtitle> subtitles;
}
class Subtitle {
Subtitle({
required this.startTime,
required this.endTime,
required this.text,
});
Time startTime;
Time endTime;
String text;
List<Object?> get props => [startTime, endTime, text];
}
class Time {
Time(Duration duration) {
this.duration = duration;
}
late Duration _duration;
Duration get duration => _duration;
set duration(Duration value) {
_duration = value;
var timeStr = duration.toString();
var timeArr = timeStr.substring(0, timeStr.lastIndexOf('.') + 4).split(':');
hour = int.parse(timeArr[0]);
minutes = int.parse(timeArr[1]);
var secArr = timeArr[2].split('.');
seconds = int.parse(secArr[0]);
milliseconds = int.parse(secArr[1]);
}
late int hour;
late int minutes;
late int seconds;
late int milliseconds;
Duration get before {
return Duration(milliseconds: duration.inMilliseconds - 1);
}
@override
String toString() {
return '${hour.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}.${milliseconds.toString().padLeft(3, '0')}';
}
}

View file

@ -0,0 +1,57 @@
import 'package:dart_framework/subtitle/parser_base.dart';
class ParserSMI extends ParserBase {
@override
RegExp get regExp => RegExp(
r'<SYNC Start=(\d+)><P Class=(.*?>)(\r|\n)*((((?!<SYNC|<\/BODY).*)(\r|\n))+)',
caseSensitive: false,
multiLine: true,
);
@override
Subtitles getSubtitlesData(String subtitlesContent) {
subtitlesContent = subtitlesContent.replaceAll(
RegExp('<font (.*?=)(.*?>)|</font>|<b>|</b>| |<i>|</i>',
caseSensitive: false, multiLine: false),
'');
final matches = regExp.allMatches(subtitlesContent).toList();
final subtitleList = <Subtitle>[];
Subtitle? lastItem;
for (final regExpMatch in matches) {
var startTime = Time(
Duration(milliseconds: int.parse(regExpMatch.group(1).toString())));
var content = regExpMatch.group(4)!;
if (lastItem != null) {
lastItem.endTime.duration = startTime.before;
lastItem = null;
}
if (!content.contains('&nbsp;')) {
content = content
.replaceAll(RegExp(r'(\r\n|\r|\n)', caseSensitive: false), '')
.replaceAll(RegExp(r'(<br>|<\/br>)', caseSensitive: false), ' ')
.trim();
content = removeAllHtmlTags(content);
if (content.isNotEmpty) {
if (subtitleList.isNotEmpty &&
content.contains(subtitleList.last.text)) {
subtitleList.last.text = content;
lastItem = subtitleList.last;
} else {
var endTime = Time(Duration());
lastItem =
Subtitle(startTime: startTime, endTime: endTime, text: content);
subtitleList.add(lastItem);
}
}
}
}
// for (var item in subtitleList) {
// print(
// '${item.startTime.toString()} --> ${item.endTime.toString()} >> ${item.text}');
// }
return Subtitles(subtitles: subtitleList);
}
}

View file

@ -0,0 +1,49 @@
// ignore: file_names
import 'package:dart_framework/subtitle/parser_base.dart';
class ParserSRT extends ParserBase {
@override
RegExp get regExp => RegExp(
r'((\d{2}):(\d{2}):(\d{2})\,(\d+)) +--> +((\d{2}):(\d{2}):(\d{2})\,(\d{3})).*[\r\n]+\s*((?:(?!\r?\n\r?).)*(\r\n|\r|\n)(?:.*))',
caseSensitive: false,
multiLine: true,
);
@override
Subtitles getSubtitlesData(String subtitlesContent) {
final matches = regExp.allMatches(subtitlesContent).toList();
final subtitleList = <Subtitle>[];
for (final regExpMatch in matches) {
final startTimeHours = int.parse(regExpMatch.group(2)!);
final startTimeMinutes = int.parse(regExpMatch.group(3)!);
final startTimeSeconds = int.parse(regExpMatch.group(4)!);
final startTimeMilliseconds = int.parse(regExpMatch.group(5)!);
final endTimeHours = int.parse(regExpMatch.group(7)!);
final endTimeMinutes = int.parse(regExpMatch.group(8)!);
final endTimeSeconds = int.parse(regExpMatch.group(9)!);
final endTimeMilliseconds = int.parse(regExpMatch.group(10)!);
final text = removeAllHtmlTags(regExpMatch.group(11)!);
final startTime = Time(Duration(
hours: startTimeHours,
minutes: startTimeMinutes,
seconds: startTimeSeconds,
milliseconds: startTimeMilliseconds,
));
final endTime = Time(Duration(
hours: endTimeHours,
minutes: endTimeMinutes,
seconds: endTimeSeconds,
milliseconds: endTimeMilliseconds,
));
subtitleList.add(
Subtitle(startTime: startTime, endTime: endTime, text: text.trim()),
);
}
return Subtitles(subtitles: subtitleList);
}
}

View file

@ -0,0 +1,10 @@
import 'package:dart_framework/subtitle/parser_srt.dart';
class ParserVTT extends ParserSRT {
@override
RegExp get regExp => RegExp(
r'((\d{2}):(\d{2}):(\d{2})\.(\d+)) +--> +((\d{2}):(\d{2}):(\d{2})\.(\d{3})).*[\r\n]+\s*((?:(?!\r?\n\r?).)*(\r\n|\r|\n)(?:.*))',
caseSensitive: false,
multiLine: true,
);
}

View file

@ -1,3 +0,0 @@
OS だと iOS SDK, Android だと Java 組み込みの機能をそのまま使って変換しているので幅広い文字コードに対応しており、バグも恐らく少なくそれでいて高速であるため。
逆に、これ以外のプラットフォームでは現状動作しないmacOS とか Windowsので、現時点であまりいないとは思われますが、デスクトップアプリもサポートしたい場合には使えません。
そういう人向けには、代わりに以下が使えます。

2923
test_eucjp.srt Normal file

File diff suppressed because it is too large Load diff