WIP 260 환경 설정 > 자막: 받은 AI 자막 모델 (Whisper · NLLB) 크기 · [지우기], 작업 중에는 못 지움 (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음6
parent
99d446cea6
commit
8937c17617
|
|
@ -96,6 +96,7 @@
|
||||||
"AI 자막": "AI subtitles",
|
"AI 자막": "AI subtitles",
|
||||||
"AI 자막 ({0}) 을 시작합니다": "Starting AI subtitles ({0})",
|
"AI 자막 ({0}) 을 시작합니다": "Starting AI subtitles ({0})",
|
||||||
"AI 자막 {0}개": "AI subtitles for {0}",
|
"AI 자막 {0}개": "AI subtitles for {0}",
|
||||||
|
"AI 자막 · 번역 작업이 끝난 뒤 지울 수 있습니다": "Can be deleted after the AI subtitle / translation job finishes",
|
||||||
"AI 자막 → MKV ({0}) 을 시작합니다": "Starting AI subtitles → MKV ({0})",
|
"AI 자막 → MKV ({0}) 을 시작합니다": "Starting AI subtitles → MKV ({0})",
|
||||||
"AI 자막 대기 중": "Waiting for AI subtitles",
|
"AI 자막 대기 중": "Waiting for AI subtitles",
|
||||||
"AI 자막 만들기": "Make AI subtitles",
|
"AI 자막 만들기": "Make AI subtitles",
|
||||||
|
|
@ -1062,10 +1063,12 @@
|
||||||
"받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "Download size: about {0}MB · downloaded files · AI models · settings are kept",
|
"받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "Download size: about {0}MB · downloaded files · AI models · settings are kept",
|
||||||
"받다 만 AI 모델": "Unfinished AI models",
|
"받다 만 AI 모델": "Unfinished AI models",
|
||||||
"받다 만 다운로드": "Unfinished downloads",
|
"받다 만 다운로드": "Unfinished downloads",
|
||||||
|
"받다 만 조각 · {0}": "Partial download · {0}",
|
||||||
"받아서 열기": "Download and open",
|
"받아서 열기": "Download and open",
|
||||||
"받은 AI 모델 ({0}) - Download/JJ_MKVMaker/AI 모델 보관": "Downloaded AI models ({0}) - Download/JJ_MKVMaker/AI 모델 보관",
|
"받은 AI 모델 ({0}) - Download/JJ_MKVMaker/AI 모델 보관": "Downloaded AI models ({0}) - Download/JJ_MKVMaker/AI 모델 보관",
|
||||||
"받은 AI 모델 ({0}) 을 Download/JJ_MKVMaker 에 복사해 두었다가 다시 설치한 뒤 되살리기 (잠시 {0} 더 필요)": "Copy downloaded AI models ({0}) to Download/JJ_MKVMaker and restore them after reinstalling (needs {0} more space for a while)",
|
"받은 AI 모델 ({0}) 을 Download/JJ_MKVMaker 에 복사해 두었다가 다시 설치한 뒤 되살리기 (잠시 {0} 더 필요)": "Copy downloaded AI models ({0}) to Download/JJ_MKVMaker and restore them after reinstalling (needs {0} more space for a while)",
|
||||||
"받은 AI 모델 (그림 · 자막) - 다시 받아야 합니다 (약 {0})": "Downloaded AI models (images · subtitles) - must be downloaded again (about {0})",
|
"받은 AI 모델 (그림 · 자막) - 다시 받아야 합니다 (약 {0})": "Downloaded AI models (images · subtitles) - must be downloaded again (about {0})",
|
||||||
|
"받은 AI 자막 모델": "Downloaded AI subtitle models",
|
||||||
"받은 동영상을 편집 목록에 추가: {0}": "Added the received video to the edit list: {0}",
|
"받은 동영상을 편집 목록에 추가: {0}": "Added the received video to the edit list: {0}",
|
||||||
"받은 업데이트 · 필수 프로그램 · Chrome 엔진을 설치하고 남은 임시 파일": "Temporary files left from installing updates · required programs · the Chrome engine",
|
"받은 업데이트 · 필수 프로그램 · Chrome 엔진을 설치하고 남은 임시 파일": "Temporary files left from installing updates · required programs · the Chrome engine",
|
||||||
"받은 자막을 한국어로 번역해 함께 추가 (한국어 자막이 없을 때)": "Translate received subtitles to Korean and add them too (when there are no Korean subtitles)",
|
"받은 자막을 한국어로 번역해 함께 추가 (한국어 자막이 없을 때)": "Translate received subtitles to Korean and add them too (when there are no Korean subtitles)",
|
||||||
|
|
@ -1076,10 +1079,12 @@
|
||||||
"받을 수 없습니다 ({0}): {1}": "Can't download ({0}): {1}",
|
"받을 수 없습니다 ({0}): {1}": "Can't download ({0}): {1}",
|
||||||
"받음": "Downloaded",
|
"받음": "Downloaded",
|
||||||
"받음 {0}": "Received {0}",
|
"받음 {0}": "Received {0}",
|
||||||
|
"받음 · {0}": "Downloaded · {0}",
|
||||||
"받지 못했습니다": "Could not download",
|
"받지 못했습니다": "Could not download",
|
||||||
"받지 못했습니다. 잠시 뒤 [이어 받기] 를 눌러 주세요": "Couldn't download. Tap [Resume] in a moment",
|
"받지 못했습니다. 잠시 뒤 [이어 받기] 를 눌러 주세요": "Couldn't download. Tap [Resume] in a moment",
|
||||||
"받지 못했습니다. 잠시 뒤 다시 해 주세요": "Download failed. Please try again shortly",
|
"받지 못했습니다. 잠시 뒤 다시 해 주세요": "Download failed. Please try again shortly",
|
||||||
"받지 못했습니다: {0}": "Could not download: {0}",
|
"받지 못했습니다: {0}": "Could not download: {0}",
|
||||||
|
"받지 않음 (처음 쓸 때 내려받기 {0})": "Not downloaded (downloads on first use, {0})",
|
||||||
"밝게": "Brighter",
|
"밝게": "Brighter",
|
||||||
"밝기": "Brightness",
|
"밝기": "Brightness",
|
||||||
"밤 0~6시": "Night 0-6",
|
"밤 0~6시": "Night 0-6",
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@
|
||||||
"AI 자막": "AI 字幕",
|
"AI 자막": "AI 字幕",
|
||||||
"AI 자막 ({0}) 을 시작합니다": "AI 字幕 ({0}) を開始します",
|
"AI 자막 ({0}) 을 시작합니다": "AI 字幕 ({0}) を開始します",
|
||||||
"AI 자막 {0}개": "AI 字幕 {0}個",
|
"AI 자막 {0}개": "AI 字幕 {0}個",
|
||||||
|
"AI 자막 · 번역 작업이 끝난 뒤 지울 수 있습니다": "AI 字幕・翻訳の作業が終わってから削除できます",
|
||||||
"AI 자막 → MKV ({0}) 을 시작합니다": "AI 字幕 → MKV ({0}) を開始します",
|
"AI 자막 → MKV ({0}) 을 시작합니다": "AI 字幕 → MKV ({0}) を開始します",
|
||||||
"AI 자막 대기 중": "AI 字幕待機中",
|
"AI 자막 대기 중": "AI 字幕待機中",
|
||||||
"AI 자막 만들기": "AI 字幕を作成",
|
"AI 자막 만들기": "AI 字幕を作成",
|
||||||
|
|
@ -1062,10 +1063,12 @@
|
||||||
"받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "ダウンロードサイズ: 約 {0}MB・ダウンロードしたファイル・AI モデル・設定はそのまま",
|
"받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "ダウンロードサイズ: 約 {0}MB・ダウンロードしたファイル・AI モデル・設定はそのまま",
|
||||||
"받다 만 AI 모델": "ダウンロード途中の AI モデル",
|
"받다 만 AI 모델": "ダウンロード途中の AI モデル",
|
||||||
"받다 만 다운로드": "ダウンロード途中のファイル",
|
"받다 만 다운로드": "ダウンロード途中のファイル",
|
||||||
|
"받다 만 조각 · {0}": "途中までのダウンロード · {0}",
|
||||||
"받아서 열기": "ダウンロードして開く",
|
"받아서 열기": "ダウンロードして開く",
|
||||||
"받은 AI 모델 ({0}) - Download/JJ_MKVMaker/AI 모델 보관": "ダウンロードした AI モデル ({0}) - Download/JJ_MKVMaker/AI 모델 보관",
|
"받은 AI 모델 ({0}) - Download/JJ_MKVMaker/AI 모델 보관": "ダウンロードした AI モデル ({0}) - Download/JJ_MKVMaker/AI 모델 보관",
|
||||||
"받은 AI 모델 ({0}) 을 Download/JJ_MKVMaker 에 복사해 두었다가 다시 설치한 뒤 되살리기 (잠시 {0} 더 필요)": "ダウンロードした AI モデル ({0}) を Download/JJ_MKVMaker にコピーし、再インストール後に復元する (一時的に {0} 余分に必要)",
|
"받은 AI 모델 ({0}) 을 Download/JJ_MKVMaker 에 복사해 두었다가 다시 설치한 뒤 되살리기 (잠시 {0} 더 필요)": "ダウンロードした AI モデル ({0}) を Download/JJ_MKVMaker にコピーし、再インストール後に復元する (一時的に {0} 余分に必要)",
|
||||||
"받은 AI 모델 (그림 · 자막) - 다시 받아야 합니다 (약 {0})": "ダウンロードした AI モデル (画像・字幕) - 再ダウンロードが必要です (約 {0})",
|
"받은 AI 모델 (그림 · 자막) - 다시 받아야 합니다 (약 {0})": "ダウンロードした AI モデル (画像・字幕) - 再ダウンロードが必要です (約 {0})",
|
||||||
|
"받은 AI 자막 모델": "ダウンロード済みの AI 字幕モデル",
|
||||||
"받은 동영상을 편집 목록에 추가: {0}": "受け取った動画を編集リストに追加: {0}",
|
"받은 동영상을 편집 목록에 추가: {0}": "受け取った動画を編集リストに追加: {0}",
|
||||||
"받은 업데이트 · 필수 프로그램 · Chrome 엔진을 설치하고 남은 임시 파일": "更新・必須プログラム・Chrome エンジンのインストールで残った一時ファイル",
|
"받은 업데이트 · 필수 프로그램 · Chrome 엔진을 설치하고 남은 임시 파일": "更新・必須プログラム・Chrome エンジンのインストールで残った一時ファイル",
|
||||||
"받은 자막을 한국어로 번역해 함께 추가 (한국어 자막이 없을 때)": "受け取った字幕を韓国語に翻訳して一緒に追加(韓国語字幕がないとき)",
|
"받은 자막을 한국어로 번역해 함께 추가 (한국어 자막이 없을 때)": "受け取った字幕を韓国語に翻訳して一緒に追加(韓国語字幕がないとき)",
|
||||||
|
|
@ -1076,10 +1079,12 @@
|
||||||
"받을 수 없습니다 ({0}): {1}": "ダウンロードできません ({0}): {1}",
|
"받을 수 없습니다 ({0}): {1}": "ダウンロードできません ({0}): {1}",
|
||||||
"받음": "ダウンロード済み",
|
"받음": "ダウンロード済み",
|
||||||
"받음 {0}": "受信 {0}",
|
"받음 {0}": "受信 {0}",
|
||||||
|
"받음 · {0}": "ダウンロード済み · {0}",
|
||||||
"받지 못했습니다": "ダウンロードできませんでした",
|
"받지 못했습니다": "ダウンロードできませんでした",
|
||||||
"받지 못했습니다. 잠시 뒤 [이어 받기] 를 눌러 주세요": "ダウンロードできませんでした。少し待ってから [続きをダウンロード] を押してください",
|
"받지 못했습니다. 잠시 뒤 [이어 받기] 를 눌러 주세요": "ダウンロードできませんでした。少し待ってから [続きをダウンロード] を押してください",
|
||||||
"받지 못했습니다. 잠시 뒤 다시 해 주세요": "ダウンロードできませんでした。しばらくしてからもう一度お試しください",
|
"받지 못했습니다. 잠시 뒤 다시 해 주세요": "ダウンロードできませんでした。しばらくしてからもう一度お試しください",
|
||||||
"받지 못했습니다: {0}": "ダウンロードできませんでした: {0}",
|
"받지 못했습니다: {0}": "ダウンロードできませんでした: {0}",
|
||||||
|
"받지 않음 (처음 쓸 때 내려받기 {0})": "未ダウンロード (初回使用時にダウンロード {0})",
|
||||||
"밝게": "明るく",
|
"밝게": "明るく",
|
||||||
"밝기": "明るさ",
|
"밝기": "明るさ",
|
||||||
"밤 0~6시": "夜 0~6 時",
|
"밤 0~6시": "夜 0~6 時",
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@
|
||||||
"AI 자막": "AI 字幕",
|
"AI 자막": "AI 字幕",
|
||||||
"AI 자막 ({0}) 을 시작합니다": "开始 AI 字幕({0})",
|
"AI 자막 ({0}) 을 시작합니다": "开始 AI 字幕({0})",
|
||||||
"AI 자막 {0}개": "AI 字幕 {0} 个",
|
"AI 자막 {0}개": "AI 字幕 {0} 个",
|
||||||
|
"AI 자막 · 번역 작업이 끝난 뒤 지울 수 있습니다": "AI 字幕 · 翻译任务结束后才能删除",
|
||||||
"AI 자막 → MKV ({0}) 을 시작합니다": "开始 AI 字幕 → MKV({0})",
|
"AI 자막 → MKV ({0}) 을 시작합니다": "开始 AI 字幕 → MKV({0})",
|
||||||
"AI 자막 대기 중": "等待 AI 字幕",
|
"AI 자막 대기 중": "等待 AI 字幕",
|
||||||
"AI 자막 만들기": "生成 AI 字幕",
|
"AI 자막 만들기": "生成 AI 字幕",
|
||||||
|
|
@ -1062,10 +1063,12 @@
|
||||||
"받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "下载大小: 约 {0}MB · 已下载的文件 · AI 模型 · 设置均保留",
|
"받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "下载大小: 约 {0}MB · 已下载的文件 · AI 模型 · 设置均保留",
|
||||||
"받다 만 AI 모델": "未下载完的 AI 模型",
|
"받다 만 AI 모델": "未下载完的 AI 模型",
|
||||||
"받다 만 다운로드": "未完成的下载",
|
"받다 만 다운로드": "未完成的下载",
|
||||||
|
"받다 만 조각 · {0}": "未完成的下载 · {0}",
|
||||||
"받아서 열기": "下载后打开",
|
"받아서 열기": "下载后打开",
|
||||||
"받은 AI 모델 ({0}) - Download/JJ_MKVMaker/AI 모델 보관": "已下载的 AI 模型 ({0}) - Download/JJ_MKVMaker/AI 모델 보관",
|
"받은 AI 모델 ({0}) - Download/JJ_MKVMaker/AI 모델 보관": "已下载的 AI 模型 ({0}) - Download/JJ_MKVMaker/AI 모델 보관",
|
||||||
"받은 AI 모델 ({0}) 을 Download/JJ_MKVMaker 에 복사해 두었다가 다시 설치한 뒤 되살리기 (잠시 {0} 더 필요)": "将已下载的 AI 模型 ({0}) 复制到 Download/JJ_MKVMaker,重新安装后恢复 (暂时需要额外 {0})",
|
"받은 AI 모델 ({0}) 을 Download/JJ_MKVMaker 에 복사해 두었다가 다시 설치한 뒤 되살리기 (잠시 {0} 더 필요)": "将已下载的 AI 模型 ({0}) 复制到 Download/JJ_MKVMaker,重新安装后恢复 (暂时需要额外 {0})",
|
||||||
"받은 AI 모델 (그림 · 자막) - 다시 받아야 합니다 (약 {0})": "已下载的 AI 模型 (图片 · 字幕) - 需要重新下载 (约 {0})",
|
"받은 AI 모델 (그림 · 자막) - 다시 받아야 합니다 (약 {0})": "已下载的 AI 模型 (图片 · 字幕) - 需要重新下载 (约 {0})",
|
||||||
|
"받은 AI 자막 모델": "已下载的 AI 字幕模型",
|
||||||
"받은 동영상을 편집 목록에 추가: {0}": "已将收到的视频加入编辑列表: {0}",
|
"받은 동영상을 편집 목록에 추가: {0}": "已将收到的视频加入编辑列表: {0}",
|
||||||
"받은 업데이트 · 필수 프로그램 · Chrome 엔진을 설치하고 남은 임시 파일": "安装更新 · 必需程序 · Chrome 引擎后残留的临时文件",
|
"받은 업데이트 · 필수 프로그램 · Chrome 엔진을 설치하고 남은 임시 파일": "安装更新 · 必需程序 · Chrome 引擎后残留的临时文件",
|
||||||
"받은 자막을 한국어로 번역해 함께 추가 (한국어 자막이 없을 때)": "将收到的字幕翻译成韩语并一并添加(没有韩语字幕时)",
|
"받은 자막을 한국어로 번역해 함께 추가 (한국어 자막이 없을 때)": "将收到的字幕翻译成韩语并一并添加(没有韩语字幕时)",
|
||||||
|
|
@ -1076,10 +1079,12 @@
|
||||||
"받을 수 없습니다 ({0}): {1}": "无法下载 ({0}): {1}",
|
"받을 수 없습니다 ({0}): {1}": "无法下载 ({0}): {1}",
|
||||||
"받음": "已下载",
|
"받음": "已下载",
|
||||||
"받음 {0}": "已接收 {0}",
|
"받음 {0}": "已接收 {0}",
|
||||||
|
"받음 · {0}": "已下载 · {0}",
|
||||||
"받지 못했습니다": "无法下载",
|
"받지 못했습니다": "无法下载",
|
||||||
"받지 못했습니다. 잠시 뒤 [이어 받기] 를 눌러 주세요": "下载失败。请稍后点按 [继续下载]",
|
"받지 못했습니다. 잠시 뒤 [이어 받기] 를 눌러 주세요": "下载失败。请稍后点按 [继续下载]",
|
||||||
"받지 못했습니다. 잠시 뒤 다시 해 주세요": "下载失败。请稍后重试",
|
"받지 못했습니다. 잠시 뒤 다시 해 주세요": "下载失败。请稍后重试",
|
||||||
"받지 못했습니다: {0}": "无法下载: {0}",
|
"받지 못했습니다: {0}": "无法下载: {0}",
|
||||||
|
"받지 않음 (처음 쓸 때 내려받기 {0})": "未下载 (首次使用时下载 {0})",
|
||||||
"밝게": "调亮",
|
"밝게": "调亮",
|
||||||
"밝기": "亮度",
|
"밝기": "亮度",
|
||||||
"밤 0~6시": "夜间 0~6 点",
|
"밤 0~6시": "夜间 0~6 点",
|
||||||
|
|
|
||||||
|
|
@ -861,7 +861,22 @@ class AppController extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final label = targets.length == 1 ? trf('AI 자막: {0}', [targets.first.fileName]) : trf('AI 자막 {0}개', [targets.length]);
|
final label = targets.length == 1 ? trf('AI 자막: {0}', [targets.first.fileName]) : trf('AI 자막 {0}개', [targets.length]);
|
||||||
return _enqueue(label, () => _runAi(targets, opts));
|
return _enqueue(label, () => _usingAiModels(() => _runAi(targets, opts)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 260: AI 자막 · 자막 번역이 모델을 쓰는 중 (그동안 환경 설정에서 그 모델을 지우지 못하게)
|
||||||
|
bool get aiModelsInUse => _aiModelUsers > 0;
|
||||||
|
int _aiModelUsers = 0;
|
||||||
|
|
||||||
|
Future<void> _usingAiModels(Future<void> Function() job) async {
|
||||||
|
_aiModelUsers++;
|
||||||
|
notifyListeners();
|
||||||
|
try {
|
||||||
|
await job();
|
||||||
|
} finally {
|
||||||
|
_aiModelUsers--;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _runAi(List<VideoItem> targets, AiOptions opts) async {
|
Future<void> _runAi(List<VideoItem> targets, AiOptions opts) async {
|
||||||
|
|
@ -1135,7 +1150,7 @@ class AppController extends ChangeNotifier {
|
||||||
if (busy) v.phase = tr('자막 번역 대기 중');
|
if (busy) v.phase = tr('자막 번역 대기 중');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return _enqueue(trf('자막 번역: {0} → {1}', [v.fileName, targets.map((t) => t.code).join('/')]),
|
return _enqueue(trf('자막 번역: {0} → {1}', [v.fileName, targets.map((t) => t.code).join('/')]),
|
||||||
() => _runTranslate(v, s, targets));
|
() => _usingAiModels(() => _runTranslate(v, s, targets)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _runTranslate(VideoItem v, SubtitleEntry s, Set<Language> targets) async {
|
Future<void> _runTranslate(VideoItem v, SubtitleEntry s, Set<Language> targets) async {
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,42 @@ class ModelStore {
|
||||||
/// 파일이 있거나, 외부 데이터 형식으로 바꿔 둔 것이 있으면 (Android 는 바꾼 뒤 원래 파일을 지운다)
|
/// 파일이 있거나, 외부 데이터 형식으로 바꿔 둔 것이 있으면 (Android 는 바꾼 뒤 원래 파일을 지운다)
|
||||||
static bool _present(String path) => File(path).existsSync() || OnnxExternal.isReady(path);
|
static bool _present(String path) => File(path).existsSync() || OnnxExternal.isReady(path);
|
||||||
|
|
||||||
|
/// 260: 이 모델이 차지한 파일 - 모델 파일 · 받다 만 조각 (.part) · Android 에서 바꿔 둔 형식 (_jj2.onnx · .data).
|
||||||
|
/// 혼자 쓰는 폴더면 폴더 안 전부
|
||||||
|
Future<List<File>> filesOf(ModelSpec m) async {
|
||||||
|
final dir = Directory(await folderOf(m));
|
||||||
|
if (!dir.existsSync()) return [];
|
||||||
|
final shared = [...whisperModels, nllbModel].any((o) => o.id != m.id && o.folder == m.folder);
|
||||||
|
if (!shared) return [for (final e in dir.listSync(recursive: true)) if (e is File) e];
|
||||||
|
return [
|
||||||
|
for (final f in m.files)
|
||||||
|
for (final path in {p.join(dir.path, f.name), '${p.join(dir.path, f.name)}.part'})
|
||||||
|
if (File(path).existsSync()) File(path),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 260: 차지한 크기 (바이트, 없으면 0)
|
||||||
|
Future<int> usedBytes(ModelSpec m) async {
|
||||||
|
var n = 0;
|
||||||
|
for (final f in await filesOf(m)) {
|
||||||
|
try {
|
||||||
|
n += f.lengthSync();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 260: 모델 지우기 (다시 쓰면 그때 다시 받는다). 혼자 쓰는 빈 폴더도 지운다
|
||||||
|
Future<void> remove(ModelSpec m) async {
|
||||||
|
for (final f in await filesOf(m)) {
|
||||||
|
await f.delete();
|
||||||
|
}
|
||||||
|
final dir = Directory(await folderOf(m));
|
||||||
|
if (dir.existsSync() && dir.listSync(recursive: true).whereType<File>().isEmpty) {
|
||||||
|
await dir.delete(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 없는 파일만 내려받는다. [onProgress] 0.0~1.0, [isCancelled] 가 true 면 중단.
|
/// 없는 파일만 내려받는다. [onProgress] 0.0~1.0, [isCancelled] 가 true 면 중단.
|
||||||
Future<void> download(
|
Future<void> download(
|
||||||
ModelSpec m, {
|
ModelSpec m, {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../app/ai_local.dart' show AiStore;
|
||||||
|
import '../app/app_controller.dart';
|
||||||
|
import '../l10n/tr.dart';
|
||||||
|
import '../services/model_store.dart';
|
||||||
|
import 'confirm.dart';
|
||||||
|
import 'setting_tile.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// 260: 받은 AI 자막 모델 (Whisper · NLLB) - 차지한 크기 · [지우기]. 다시 쓰면 그때 다시 받는다
|
||||||
|
class AiSubtitleModels extends StatefulWidget {
|
||||||
|
final AppController c;
|
||||||
|
const AiSubtitleModels({super.key, required this.c});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AiSubtitleModels> createState() => _AiSubtitleModelsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AiSubtitleModelsState extends State<AiSubtitleModels> {
|
||||||
|
/// 모델 id → 차지한 바이트 (0 이면 받지 않음)
|
||||||
|
Map<String, int>? _used;
|
||||||
|
|
||||||
|
/// 다 받은 모델 (나머지 중 크기가 있으면 받다 만 조각)
|
||||||
|
final _installed = <String>{};
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
ModelStore get _store => widget.c.services.models;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refresh() async {
|
||||||
|
final used = {for (final m in [...whisperModels, nllbModel]) m.id: await _store.usedBytes(m)};
|
||||||
|
_installed
|
||||||
|
..clear()
|
||||||
|
..addAll([for (final m in [...whisperModels, nllbModel]) if (await _store.isInstalled(m)) m.id]);
|
||||||
|
if (mounted) setState(() => _used = used);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _remove(ModelSpec m) async {
|
||||||
|
final ok = await confirmAction(context,
|
||||||
|
title: trf('{0} 을(를) 지울까요?', [m.label]), body: tr('다시 쓰려면 다시 받아야 합니다.'), ok: tr('지우기'));
|
||||||
|
if (!ok) return;
|
||||||
|
try {
|
||||||
|
await _store.remove(m);
|
||||||
|
_error = null;
|
||||||
|
} catch (e) {
|
||||||
|
_error = trf('지우지 못했습니다: {0}', [e]);
|
||||||
|
}
|
||||||
|
await _refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final used = _used;
|
||||||
|
if (used == null) return const SizedBox();
|
||||||
|
return ListenableBuilder(
|
||||||
|
listenable: widget.c,
|
||||||
|
builder: (context, _) {
|
||||||
|
final inUse = widget.c.aiModelsInUse;
|
||||||
|
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
for (final m in [...whisperModels, nllbModel])
|
||||||
|
SettingTile(
|
||||||
|
dense: true,
|
||||||
|
leading: Icon(used[m.id]! > 0 ? Icons.check_circle : Icons.cloud_download_outlined,
|
||||||
|
color: used[m.id]! > 0 ? JjColors.success : JjColors.textDim, size: 20),
|
||||||
|
title: Text(m.label),
|
||||||
|
subtitle: Text(used[m.id]! > 0
|
||||||
|
? trf(_installed.contains(m.id) ? '받음 · {0}' : '받다 만 조각 · {0}', [AiStore.sizeText(used[m.id]!)])
|
||||||
|
: trf('받지 않음 (처음 쓸 때 내려받기 {0})', [m.sizeLabel])),
|
||||||
|
trailing: used[m.id]! > 0
|
||||||
|
? inUse
|
||||||
|
? Tooltip(
|
||||||
|
message: tr('AI 자막 · 번역 작업이 끝난 뒤 지울 수 있습니다'),
|
||||||
|
child: TextButton(onPressed: null, child: Text(tr('지우기'))),
|
||||||
|
)
|
||||||
|
: TextButton(onPressed: () => _remove(m), child: Text(tr('지우기')))
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
if (_error != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 4, 16, 0),
|
||||||
|
child: Text(_error!, style: const TextStyle(fontSize: 12, color: JjColors.danger)),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,7 @@ import 'browser_page.dart' show deleteExportedCookies;
|
||||||
import 'security_settings.dart';
|
import 'security_settings.dart';
|
||||||
import 'network_settings.dart';
|
import 'network_settings.dart';
|
||||||
import 'ai_settings.dart';
|
import 'ai_settings.dart';
|
||||||
|
import 'ai_subtitle_models.dart';
|
||||||
import '../app/master_lock.dart';
|
import '../app/master_lock.dart';
|
||||||
import 'toast_status.dart';
|
import 'toast_status.dart';
|
||||||
import 'license_texts.dart';
|
import 'license_texts.dart';
|
||||||
|
|
@ -541,6 +542,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
title: Text(tr('시작할 때마다 설정 창 보기')),
|
title: Text(tr('시작할 때마다 설정 창 보기')),
|
||||||
subtitle: Text(trf('끄면 마지막 설정으로 바로 시작: 원어 {0}, ' '언어 {1}, {2}', [c.aiOptions.source.name, c.aiOptions.targets.map((t) => t.code).join('/'), c.aiOptions.whisper.label])),
|
subtitle: Text(trf('끄면 마지막 설정으로 바로 시작: 원어 {0}, ' '언어 {1}, {2}', [c.aiOptions.source.name, c.aiOptions.targets.map((t) => t.code).join('/'), c.aiOptions.whisper.label])),
|
||||||
),
|
),
|
||||||
|
// 260: 받은 자막 모델 크기 · 지우기
|
||||||
|
_subTitle(tr('받은 AI 자막 모델')),
|
||||||
|
AiSubtitleModels(c: c),
|
||||||
_subTitle(tr('인터넷 자막 (OpenSubtitles.com)')),
|
_subTitle(tr('인터넷 자막 (OpenSubtitles.com)')),
|
||||||
_textTile(tr('API 키 (필수, 무료)'), s.openSubtitlesKey,
|
_textTile(tr('API 키 (필수, 무료)'), s.openSubtitlesKey,
|
||||||
tr('가입 → 프로필 → API consumers → New consumer'),
|
tr('가입 → 프로필 → API consumers → New consumer'),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jj_mkvmaker/app/app_controller.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/model_store.dart';
|
||||||
|
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/ai_subtitle_models.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
/// 260: 받은 AI 자막 모델 (Whisper · NLLB) 을 앱 안에서 크기 보고 지우기
|
||||||
|
void main() {
|
||||||
|
late Directory tmp;
|
||||||
|
late ModelStore models;
|
||||||
|
setUp(() async {
|
||||||
|
tmp = Directory.systemTemp.createTempSync('jj_m260_');
|
||||||
|
models = ModelStore(tmp.path);
|
||||||
|
// whisper base 만 받음 (small 은 받다 만 조각), NLLB 는 Android 가 바꿔 둔 파일까지
|
||||||
|
File(await models.pathOf(whisperModels[0]))
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytesSync(List.filled(3000, 1));
|
||||||
|
File('${await models.pathOf(whisperModels[1])}.part').writeAsBytesSync(List.filled(500, 1));
|
||||||
|
final nllb = await models.folderOf(nllbModel);
|
||||||
|
for (final n in ['tokenizer.json', 'encoder_model_quantized_jj2.onnx', 'encoder_model_quantized_jj2.onnx.data',
|
||||||
|
'decoder_model_merged_quantized_jj2.onnx', 'decoder_model_merged_quantized_jj2.onnx.data']) {
|
||||||
|
File(p.join(nllb, n))
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytesSync(List.filled(1000, 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
|
||||||
|
test('크기 · 지우기: 같은 폴더의 다른 Whisper 는 남기고, NLLB 는 폴더째', () async {
|
||||||
|
expect(await models.usedBytes(whisperModels[0]), 3000);
|
||||||
|
expect(await models.usedBytes(whisperModels[1]), 500, reason: '받다 만 조각도 셈');
|
||||||
|
expect(await models.usedBytes(nllbModel), 5000);
|
||||||
|
await models.remove(whisperModels[0]);
|
||||||
|
expect(File(await models.pathOf(whisperModels[0])).existsSync(), isFalse);
|
||||||
|
expect(File('${await models.pathOf(whisperModels[1])}.part').existsSync(), isTrue, reason: '다른 모델 것은 남김');
|
||||||
|
await models.remove(nllbModel);
|
||||||
|
expect(Directory(await models.folderOf(nllbModel)).existsSync(), isFalse);
|
||||||
|
expect(await models.usedBytes(nllbModel), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('환경 설정 목록: 받은 것은 크기 · [지우기] → 확인 뒤 지움', (tester) async {
|
||||||
|
final c = AppController(PlatformServices(
|
||||||
|
mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), models: models));
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
await tester.pumpWidget(MaterialApp(home: Scaffold(body: ListView(children: [AiSubtitleModels(c: c)]))));
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text(whisperModels[0].label), findsOneWidget);
|
||||||
|
expect(find.textContaining('받음'), findsNWidgets(2));
|
||||||
|
expect(find.textContaining('받다 만 조각'), findsOneWidget);
|
||||||
|
expect(find.text('지우기'), findsNWidgets(3));
|
||||||
|
await tester.tap(find.text('지우기').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.widgetWithText(FilledButton, '지우기'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// 지우기는 실제 파일을 하나씩 지운다 - 다 지워질 때까지 실제 시간을 조금씩 준다
|
||||||
|
final nllbDir = Directory((await tester.runAsync(() => models.folderOf(nllbModel)))!);
|
||||||
|
for (var i = 0; i < 50 && find.textContaining('받지 않음').evaluate().isEmpty; i++) {
|
||||||
|
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
expect(nllbDir.existsSync(), isFalse);
|
||||||
|
expect(find.text('지우기'), findsNWidgets(2));
|
||||||
|
expect(find.textContaining('받지 않음'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue