From 65c049d0437caa76aeb3874355a6e00e59ba9848 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:14:57 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9E=91=EC=97=85=20=EA=B8=B0=EB=A1=9D?= =?UTF-8?q?=EC=9D=98=20=EA=B2=BD=EB=A1=9C=EB=8A=94=20=EA=B8=B0=EA=B8=B0=20?= =?UTF-8?q?=EA=B5=AC=EB=B6=84=EC=9E=90=EB=A1=9C=20=C2=B7=20AI=20=EC=98=88?= =?UTF-8?q?=EC=83=81=20=EC=8B=9C=EA=B0=84=20'=EC=95=BD=201~1=EB=B6=84'=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8=20(196=20=C2=B7=20197)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 196 (버그): Android 에서도 'jj_mkv\이름' - p.join - 197 (버그): 짧은 동영상의 '약 1~1분' → '1분 안', 같은 수면 하나 Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 8 +++++--- assets/l10n/ja.json | 8 +++++--- assets/l10n/zh-Hans.json | 8 +++++--- lib/app/app_controller.dart | 5 +++-- lib/ui/ai_dialog.dart | 12 ++++++++++-- test/ai_confirm_test.dart | 4 ++++ 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 5827bf9..35bd94b 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -65,6 +65,7 @@ "0 으로": "Reset to 0", "10초 뒤로 (←)": "Back 10 seconds (←)", "10초 앞으로 (→)": "Forward 10 seconds (→)", + "1분 안": "under 1 min", "5초 뒤로": "Back 5 seconds", "5초 앞으로": "Forward 5 seconds", "90° 회전 (시계 방향)": "Rotate 90° (clockwise)", @@ -102,7 +103,7 @@ "AI 자막 만들기{0}{1}": "Make AI subtitles{0}{1}", "AI 자막 시작: {0}": "AI subtitles started: {0}", "AI 자막 실패: {0}\n{1}": "AI subtitles failed: {0}\n{1}", - "AI 자막 완료: {0} → {1}\\{2}_AI.srt 외 {3}개": "AI subtitles done: {0} → {1}\\{2}_AI.srt and {3} more", + "AI 자막 완료: {0} → {1} 외 {2}개": "AI subtitles done: {0} → {1} and {2} more", "AI 자막 중단: {0}": "AI subtitles stopped: {0}", "AI 자막 취소: {0}": "AI subtitles cancelled: {0}", "AI 자막: {0}": "AI subtitles: {0}", @@ -320,6 +321,7 @@ "yt-dlp 의 YouTube 화질 목록 추출": "Extracts the YouTube quality list for yt-dlp", "{0} (AI 번역 ← {1})": "{0} (AI translation ← {1})", "{0} (기본): {1}": "{0} (default): {1}", + "{0} (동영상 {1})": "{0} (videos: {1})", "{0} - {1} (약 {2}MB)": "{0} - {1} (about {2}MB)", "{0} / {1}장": "{0} / {1} images", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2} (change at the top of the main screen)", @@ -1176,7 +1178,7 @@ "시작 · 웹 브라우저": "Startup · web browser", "시작 메뉴에 등록": "Add to Start menu", "시작 안 함 (모니터링에서 시작)": "Don't start (start from Monitor)", - "시작: {0} → {1}\\{2}{3}": "Started: {0} → {1}\\{2}{3}", + "시작: {0} → {1}{2}": "Started: {0} → {1}{2}", "시작하는 중…": "Starting…", "시작하지 못했습니다: {0}": "Could not start: {0}", "시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).": "Choose which syncs to start. The others stay paused for this session (start them in Monitor > lsync).", @@ -1262,7 +1264,7 @@ "앱이 켜져 있는 동안 늘 동기화합니다.": "Always syncs while the app is running.", "약 {0} 남음": "about {0} left", "약 {0}~{1}분": "about {0}–{1} min", - "약 {0}~{1}분 (동영상 {2})": "about {0}–{1} min (videos: {2})", + "약 {0}분": "about {0} min", "양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).": "Both ways (⇄) never deletes source files (--remove-source-files in the options is not used).", "양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "Both ways: newer files on the receiving side are not overwritten (-u).", "어둡게": "Darker", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 92e2e67..2dea2f0 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -65,6 +65,7 @@ "0 으로": "0 に戻す", "10초 뒤로 (←)": "10秒戻る(←)", "10초 앞으로 (→)": "10秒進む(→)", + "1분 안": "1 分以内", "5초 뒤로": "5秒戻る", "5초 앞으로": "5秒進む", "90° 회전 (시계 방향)": "90° 回転 (時計回り)", @@ -102,7 +103,7 @@ "AI 자막 만들기{0}{1}": "AI 字幕を作成{0}{1}", "AI 자막 시작: {0}": "AI 字幕開始: {0}", "AI 자막 실패: {0}\n{1}": "AI 字幕失敗: {0}\n{1}", - "AI 자막 완료: {0} → {1}\\{2}_AI.srt 외 {3}개": "AI 字幕完了: {0} → {1}\\{2}_AI.srt ほか {3}個", + "AI 자막 완료: {0} → {1} 외 {2}개": "AI 字幕完了: {0} → {1} ほか {2}個", "AI 자막 중단: {0}": "AI 字幕中断: {0}", "AI 자막 취소: {0}": "AI 字幕キャンセル: {0}", "AI 자막: {0}": "AI 字幕: {0}", @@ -320,6 +321,7 @@ "yt-dlp 의 YouTube 화질 목록 추출": "yt-dlp 用の YouTube 画質リストを取得", "{0} (AI 번역 ← {1})": "{0}(AI 翻訳 ← {1})", "{0} (기본): {1}": "{0}(既定): {1}", + "{0} (동영상 {1})": "{0} (動画 {1})", "{0} - {1} (약 {2}MB)": "{0} - {1}(約 {2}MB)", "{0} / {1}장": "{0} / {1} 枚", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(メイン画面の上部で変更)", @@ -1176,7 +1178,7 @@ "시작 · 웹 브라우저": "起動・ウェブブラウザー", "시작 메뉴에 등록": "スタートメニューに登録", "시작 안 함 (모니터링에서 시작)": "開始しない (モニタリングで開始)", - "시작: {0} → {1}\\{2}{3}": "開始: {0} → {1}\\{2}{3}", + "시작: {0} → {1}{2}": "開始: {0} → {1}{2}", "시작하는 중…": "開始中…", "시작하지 못했습니다: {0}": "開始できませんでした: {0}", "시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).": "開始する同期を選んでください。選ばなかったものは今回の実行中は停止します (モニタリング > lsync で開始)。", @@ -1262,7 +1264,7 @@ "앱이 켜져 있는 동안 늘 동기화합니다.": "アプリの起動中は常に同期します。", "약 {0} 남음": "残り約 {0}", "약 {0}~{1}분": "約 {0}~{1} 分", - "약 {0}~{1}분 (동영상 {2})": "約 {0}~{1} 分 (動画 {2})", + "약 {0}분": "約 {0} 分", "양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).": "双方向 (⇄) では元のファイルを削除しません (オプションの --remove-source-files は使いません)。", "양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "双方向: 受け取る側の方が新しいファイルは上書きしません (-u)。", "어둡게": "暗く", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 6c7cac3..6cc35a9 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -65,6 +65,7 @@ "0 으로": "归零", "10초 뒤로 (←)": "后退 10 秒(←)", "10초 앞으로 (→)": "前进 10 秒(→)", + "1분 안": "1 分钟以内", "5초 뒤로": "后退 5 秒", "5초 앞으로": "前进 5 秒", "90° 회전 (시계 방향)": "旋转 90° (顺时针)", @@ -102,7 +103,7 @@ "AI 자막 만들기{0}{1}": "生成 AI 字幕{0}{1}", "AI 자막 시작: {0}": "AI 字幕开始: {0}", "AI 자막 실패: {0}\n{1}": "AI 字幕失败: {0}\n{1}", - "AI 자막 완료: {0} → {1}\\{2}_AI.srt 외 {3}개": "AI 字幕完成: {0} → {1}\\{2}_AI.srt 等 {3} 个", + "AI 자막 완료: {0} → {1} 외 {2}개": "AI 字幕完成: {0} → {1} 等 {2} 个", "AI 자막 중단: {0}": "AI 字幕中断: {0}", "AI 자막 취소: {0}": "AI 字幕已取消: {0}", "AI 자막: {0}": "AI 字幕: {0}", @@ -320,6 +321,7 @@ "yt-dlp 의 YouTube 화질 목록 추출": "提取 yt-dlp 的 YouTube 画质列表", "{0} (AI 번역 ← {1})": "{0}(AI 翻译 ← {1})", "{0} (기본): {1}": "{0}(默认): {1}", + "{0} (동영상 {1})": "{0}(视频 {1})", "{0} - {1} (약 {2}MB)": "{0} - {1}(约 {2}MB)", "{0} / {1}장": "{0} / {1} 张", "{0} · {1}{2} (메인 화면 상단에서 변경)": "{0} · {1}{2}(在主界面顶部更改)", @@ -1176,7 +1178,7 @@ "시작 · 웹 브라우저": "启动 · 网页浏览器", "시작 메뉴에 등록": "添加到开始菜单", "시작 안 함 (모니터링에서 시작)": "不开始 (在监控中开始)", - "시작: {0} → {1}\\{2}{3}": "开始: {0} → {1}\\{2}{3}", + "시작: {0} → {1}{2}": "开始: {0} → {1}{2}", "시작하는 중…": "正在开始…", "시작하지 못했습니다: {0}": "无法开始:{0}", "시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).": "请选择要开始的同步。未选的在本次运行期间暂停 (可在 监控 > lsync 中开始)。", @@ -1262,7 +1264,7 @@ "앱이 켜져 있는 동안 늘 동기화합니다.": "应用运行期间始终同步。", "약 {0} 남음": "约剩 {0}", "약 {0}~{1}분": "约 {0}~{1} 分钟", - "약 {0}~{1}분 (동영상 {2})": "约 {0}~{1} 分钟(视频 {2})", + "약 {0}분": "约 {0} 分钟", "양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).": "双向 (⇄) 不会删除源文件(不使用选项中的 --remove-source-files)。", "양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "双向: 接收端较新的文件不会被覆盖 (-u)。", "어둡게": "调暗", diff --git a/lib/app/app_controller.dart b/lib/app/app_controller.dart index 19f991c..5b820cd 100644 --- a/lib/app/app_controller.dart +++ b/lib/app/app_controller.dart @@ -990,7 +990,8 @@ class AppController extends ChangeNotifier { ..status = JobStatus.ready ..phase = null ..progress = 0; - _log(trf('AI 자막 완료: {0} → {1}\\{2}_AI.srt 외 {3}개', [v.fileName, outputFolderName, v.baseName, langs.length])); + // 196: 경로는 기기의 구분자로 (Android 에서도 "jj_mkv\이름" 으로 보였다) + _log(trf('AI 자막 완료: {0} → {1} 외 {2}개', [v.fileName, p.join(outputFolderName, '${v.baseName}_AI.srt'), langs.length])); } on AiCancelled { v ..status = JobStatus.ready @@ -1388,7 +1389,7 @@ class AppController extends ChangeNotifier { ..progress = 0 ..message = null; notifyListeners(); - _log(trf('시작: {0} → {1}\\{2}' '{3}', [v.fileName, outputFolderName, p.basename(out), encode.reencode ? ' [${encode.codec.label} · ${encode.resolution.label} · ${encode.quality.label}${encode.adjusts ? ' · ${encode.adjustSummary}' : ''}]' : ''])); + _log(trf('시작: {0} → {1}{2}', [v.fileName, p.join(outputFolderName, p.basename(out)), encode.reencode ? ' [${encode.codec.label} · ${encode.resolution.label} · ${encode.quality.label}${encode.adjusts ? ' · ${encode.adjustSummary}' : ''}]' : ''])); final copies = {}; var started = false; // FFmpeg 가 출력 파일을 쓰기 시작했는지 try { diff --git a/lib/ui/ai_dialog.dart b/lib/ui/ai_dialog.dart index 5d4bada..b629f8b 100644 --- a/lib/ui/ai_dialog.dart +++ b/lib/ui/ai_dialog.dart @@ -50,7 +50,7 @@ const aiStartDelay = Duration(seconds: 3); String aiStartNotice(List videos, AiOptions o, {required bool thenBuild}) { final known = videos.map((v) => v.info?.duration).whereType().toList(); final est = known.length == videos.length ? aiEstimateMinutes(known.fold(Duration.zero, (a, b) => a + b), aiTranslateCount(o)) : null; - final what = [trf('동영상 {0}개', [videos.length]), if (est != null) trf('약 {0}~{1}분', [est.$1, est.$2])].join(' · '); + final what = [trf('동영상 {0}개', [videos.length]), if (est != null) aiEstimateText(est)].join(' · '); return thenBuild ? trf('AI 자막 → MKV ({0}) 을 시작합니다', [what]) : trf('AI 자막 ({0}) 을 시작합니다', [what]); } @@ -64,6 +64,14 @@ const aiColor = Color(0xFFB388FF); return ((h * (15 + 10 * translateLanguages)).ceil(), (h * (30 + 20 * translateLanguages)).ceil()); } +/// 197: 예상 시간 글 - 짧은 동영상은 "약 1~1분" 처럼 같은 수가 두 번 보였다 +String aiEstimateText((int, int) est) { + final (lo, hi) = est; + if (hi <= 1) return tr('1분 안'); + if (lo == hi) return trf('약 {0}분', [lo]); + return trf('약 {0}~{1}분', [lo, hi]); +} + /// 번역할 언어 수 (원어와 같은 언어는 번역하지 않음, 원어 자동 감지면 하나는 원어로 본다) int aiTranslateCount(AiOptions o) => o.source == undetermined ? (o.targets.length - 1).clamp(0, 99) @@ -90,7 +98,7 @@ class AiPlanSummary extends StatelessWidget { tr('예상 시간'), est == null ? tr('동영상 길이를 몰라 셀 수 없습니다 (영상 1시간 ≈ 음성인식 15~30분 + 언어당 번역 10~20분)') - : trf('약 {0}~{1}분 (동영상 {2})', [est.$1, est.$2, hm(total)]), + : trf('{0} (동영상 {1})', [aiEstimateText(est), hm(total)]), ), ]; return Container( diff --git a/test/ai_confirm_test.dart b/test/ai_confirm_test.dart index e09bc61..ae0a642 100644 --- a/test/ai_confirm_test.dart +++ b/test/ai_confirm_test.dart @@ -15,6 +15,10 @@ void main() { expect(aiEstimateMinutes(const Duration(hours: 1), 0), (15, 30)); expect(aiEstimateMinutes(const Duration(hours: 2), 2), (70, 140)); expect(aiEstimateMinutes(Duration.zero, 2), isNull); + // 197: 짧은 동영상은 '약 1~1분' 이 아니라 '1분 안', 같은 수면 하나만 + expect(aiEstimateText(aiEstimateMinutes(const Duration(seconds: 30), 0)!), '1분 안'); + expect(aiEstimateText((3, 3)), '약 3분'); + expect(aiEstimateText((15, 30)), '약 15~30분'); final ko = languageOf('ko'), en = languageOf('en'), ja = languageOf('ja'); expect(aiTranslateCount(AiOptions(source: ko, targets: {ko, en, ja}, whisper: whisperModels.first)), 2); expect(aiTranslateCount(AiOptions(targets: {ko, en, ja}, whisper: whisperModels.first)), 2, reason: '자동 감지면 하나는 원어로');