142 lines
6.0 KiB
Dart
142 lines
6.0 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:jj_mkvmaker/app/app_controller.dart';
|
|
import 'package:jj_mkvmaker/core/ai_subtitle.dart';
|
|
import 'package:jj_mkvmaker/core/languages.dart';
|
|
import 'package:jj_mkvmaker/core/models.dart';
|
|
import 'package:jj_mkvmaker/core/output_paths.dart';
|
|
import 'package:jj_mkvmaker/core/srt.dart';
|
|
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
|
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
|
import 'package:jj_mkvmaker/services/ai_services.dart';
|
|
import 'package:jj_mkvmaker/services/model_store.dart';
|
|
import 'package:jj_mkvmaker/services/platform_services.dart';
|
|
import 'package:path/path.dart' as p;
|
|
|
|
class _FakeRecognizer implements SpeechRecognizer {
|
|
String? lastLanguage;
|
|
@override
|
|
Future<List<Cue>> transcribe(String wavPath,
|
|
{required String modelPath, String language = 'auto', AiProgress? onProgress}) async {
|
|
lastLanguage = language;
|
|
expect(File(wavPath).existsSync(), isTrue, reason: '음성 추출 파일');
|
|
onProgress?.call(1);
|
|
return [
|
|
Cue(const Duration(seconds: 1), const Duration(seconds: 2), ' Hello everyone. '),
|
|
Cue(const Duration(seconds: 2), const Duration(seconds: 3), '[BLANK_AUDIO]'),
|
|
Cue(const Duration(seconds: 3), const Duration(seconds: 5), 'This is a test of the subtitle program.'),
|
|
];
|
|
}
|
|
}
|
|
|
|
class _FakeTranslator implements Translator {
|
|
final calls = <String>[];
|
|
bool disposed = false;
|
|
@override
|
|
Future<void> load(String modelDir) async {}
|
|
@override
|
|
Future<List<String>> translate(List<String> lines,
|
|
{required String source, required String target, AiProgress? onProgress}) async {
|
|
calls.add('$source>$target');
|
|
onProgress?.call(1);
|
|
return [for (final l in lines) '[$target] $l'];
|
|
}
|
|
|
|
@override
|
|
void cancel() {}
|
|
@override
|
|
Future<void> dispose() async => disposed = true;
|
|
}
|
|
|
|
void main() {
|
|
group('원어 판별', () {
|
|
test('문자 체계', () {
|
|
expect(detectLanguage('안녕하세요 반갑습니다').code, 'ko');
|
|
expect(detectLanguage('こんにちは、元気ですか').code, 'ja');
|
|
expect(detectLanguage('你好,我们今天学习').code, 'zh-Hans');
|
|
expect(detectLanguage('Привет, как дела').code, 'ru');
|
|
expect(detectLanguage('...').code, 'und');
|
|
});
|
|
test('라틴 문자', () {
|
|
expect(detectLanguage('This is what we are going to do and it is fun').code, 'en');
|
|
expect(detectLanguage('Hola, ¿qué tal? Esta es la casa de los padres').code, 'es');
|
|
expect(detectLanguage("Je ne sais pas, c'est une question que nous avons").code, 'fr');
|
|
expect(detectLanguage('Ich weiß nicht, das ist nicht die Frage und wir').code, 'de');
|
|
});
|
|
});
|
|
|
|
test('인식 결과 정리: 비음성 표시 제거, 긴 줄 나누기', () {
|
|
final out = cleanRecognized([
|
|
Cue(Duration.zero, const Duration(seconds: 1), ' [Music] '),
|
|
Cue(Duration.zero, const Duration(seconds: 1), '(음악)'),
|
|
Cue(Duration.zero, const Duration(seconds: 1), ''),
|
|
Cue(Duration.zero, const Duration(seconds: 3),
|
|
'This sentence is definitely much longer than forty two characters in total'),
|
|
]);
|
|
expect(out, hasLength(1));
|
|
expect(out.single.text.split('\n'), hasLength(2));
|
|
expect(out.single.text.replaceAll('\n', ' '),
|
|
'This sentence is definitely much longer than forty two characters in total');
|
|
});
|
|
|
|
test('흐름: 영어 영상 → _AI.srt + _ko/_en/_ja.srt, MKV 목록에 추가', () async {
|
|
final tool = ProcessMediaTool('ffmpeg', 'ffprobe');
|
|
if (await tool.version() == null) return markTestSkipped('FFmpeg 없음');
|
|
|
|
final dir = Directory.systemTemp.createTempSync('jj_ai_');
|
|
addTearDown(() => dir.deleteSync(recursive: true));
|
|
final video = p.join(dir.path, '강의.mp4');
|
|
await tool.runFfmpeg(['-y', '-f', 'lavfi', '-i', 'testsrc=size=160x120:rate=10',
|
|
'-f', 'lavfi', '-i', 'sine', '-t', '3', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-c:a', 'aac', video]);
|
|
|
|
// 모델 파일이 이미 있는 것처럼 준비 (내려받기 건너뜀)
|
|
final models = ModelStore(p.join(dir.path, 'models'));
|
|
for (final m in [whisperModels.first, nllbModel]) {
|
|
for (var i = 0; i < m.files.length; i++) {
|
|
File(await models.pathOf(m, i))
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('x');
|
|
}
|
|
}
|
|
final rec = _FakeRecognizer();
|
|
final tr = _FakeTranslator();
|
|
final c = AppController(PlatformServices(
|
|
mediaTool: tool,
|
|
storage: DesktopStorageService(),
|
|
createRecognizer: () => rec,
|
|
createTranslator: () => tr,
|
|
models: models,
|
|
));
|
|
await c.init();
|
|
await c.addVideos([video]);
|
|
final v = c.videos.single;
|
|
|
|
await c.generateAiSubtitles([v], AiOptions.defaults());
|
|
|
|
expect(v.status, JobStatus.ready, reason: v.message);
|
|
expect(rec.lastLanguage, 'auto');
|
|
// 원어(영어) 는 번역하지 않음
|
|
expect(tr.calls, ['eng_Latn>kor_Hang', 'eng_Latn>jpn_Jpan']);
|
|
expect(tr.disposed, isTrue);
|
|
|
|
final ai = parseSrt(File(aiSubtitlePath(video)).readAsStringSync());
|
|
expect(ai.map((c) => c.text), ['Hello everyone.', 'This is a test of the subtitle program.']);
|
|
String read(String code) => File(languageSubtitlePath(video, languageOf(code))).readAsStringSync();
|
|
expect(read('ko'), contains('[kor_Hang] Hello everyone.'));
|
|
expect(read('ja'), contains('[jpn_Jpan] This is a test'));
|
|
expect(read('en'), contains('Hello everyone.'));
|
|
expect(read('en'), isNot(contains('[')));
|
|
|
|
// MKV 목록: 언어별 3개 (AI 원본 파일은 넣지 않음)
|
|
final ext = v.subtitles.where((s) => s.kind == SubtitleKind.external).toList();
|
|
expect(ext.map((s) => p.basename(s.path!)), ['강의_ko.srt', '강의_en.srt', '강의_ja.srt']);
|
|
expect(ext[1].title, '영어 (AI 인식)');
|
|
expect(ext[0].title, '한국어 (AI 번역)');
|
|
|
|
// 다시 실행해도 중복 추가되지 않음
|
|
await c.generateAiSubtitles([v], AiOptions.defaults());
|
|
expect(v.subtitles.where((s) => s.kind == SubtitleKind.external), hasLength(3));
|
|
});
|
|
}
|