작업 기록의 경로는 기기 구분자로 · AI 예상 시간 '약 1~1분' 고침 (196 · 197)

- 196 (버그): Android 에서도 'jj_mkv\이름' - p.join
- 197 (버그): 짧은 동영상의 '약 1~1분' → '1분 안', 같은 수면 하나

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음5
RyuJongJin 2026-10-12 02:14:57 +09:00
parent 6cb2dde089
commit 65c049d043
6 changed files with 32 additions and 13 deletions

View File

@ -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",

View File

@ -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)。",
"어둡게": "暗く",

View File

@ -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)。",
"어둡게": "调暗",

View File

@ -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 = <SubtitleEntry, String>{};
var started = false; // FFmpeg 가 출력 파일을 쓰기 시작했는지
try {

View File

@ -50,7 +50,7 @@ const aiStartDelay = Duration(seconds: 3);
String aiStartNotice(List<VideoItem> videos, AiOptions o, {required bool thenBuild}) {
final known = videos.map((v) => v.info?.duration).whereType<Duration>().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(

View File

@ -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: '자동 감지면 하나는 원어로');