진행 표시: 해상도 올리기는 모델을 읽는 동안 숫자 없이 (167) · 큰 파일 하나도 보낸 바이트로 오름 · 끝난 진행 창의 이름 (171)
- 167: sd-cli 의 모델 읽기 막대를 올리기 진행으로 읽어 처음부터 100% 였다. 읽는 동안은 움직이는 막대, 올리기는 타일 n/m 으로 % 와 "약 n초 남음" (upscaleProgressOf) - 앱이 직접 복사할 때 항목마다 바이트 크기와 보낸 바이트로 진행률 (이름 바꾸기로 옮기면 파일 수로), 보내는 동안 0.2초마다 알림 - 큰 파일 하나가 끝날 때까지 0% 에 머물던 것 - 171: 끝난 진행 창의 " 안의 파일" 에 마지막 항목 이름 - 시험: upscaleProgressOf, 256KB 를 256KB/s 로 복사하는 도중 20~100% Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음4
parent
794f484c19
commit
f331672e3a
|
|
@ -1138,6 +1138,7 @@
|
|||
"앱을 새로 설치해 설정이 없습니다. 저장해 둔 v{0} 의 설정 (동영상 목록 · 즐겨찾기 포함) 을 되살릴까요?\n(자막 API 키 · 비밀번호는 저장하지 않았으니 다시 넣어 주세요)": "The app was newly installed and has no settings. Restore the saved settings of v{0} (including the video list and bookmarks)?\n(Subtitle API keys and passwords were not saved - please enter them again)",
|
||||
"앱을 지우면 다음은 되살릴 수 없습니다:": "Removing the app loses the following for good:",
|
||||
"앱이 켜져 있는 동안 늘 동기화합니다.": "Always syncs while the app is running.",
|
||||
"약 {0} 남음": "about {0} left",
|
||||
"양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "Both ways: newer files on the receiving side are not overwritten (-u).",
|
||||
"어둡게": "Darker",
|
||||
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "Press anywhere to bring up the JJ_MKVMaker window",
|
||||
|
|
|
|||
|
|
@ -1138,6 +1138,7 @@
|
|||
"앱을 새로 설치해 설정이 없습니다. 저장해 둔 v{0} 의 설정 (동영상 목록 · 즐겨찾기 포함) 을 되살릴까요?\n(자막 API 키 · 비밀번호는 저장하지 않았으니 다시 넣어 주세요)": "アプリを新しくインストールしたため設定がありません。保存した v{0} の設定 (動画リスト · ブックマークを含む) を復元しますか?\n(字幕 API キー · パスワードは保存していないので再入力してください)",
|
||||
"앱을 지우면 다음은 되살릴 수 없습니다:": "アプリを削除すると次のものは復元できません:",
|
||||
"앱이 켜져 있는 동안 늘 동기화합니다.": "アプリの起動中は常に同期します。",
|
||||
"약 {0} 남음": "残り約 {0}",
|
||||
"양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "双方向: 受け取る側の方が新しいファイルは上書きしません (-u)。",
|
||||
"어둡게": "暗く",
|
||||
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "どこからでも押すと JJ_MKVMaker ウィンドウが表示されます",
|
||||
|
|
|
|||
|
|
@ -1138,6 +1138,7 @@
|
|||
"앱을 새로 설치해 설정이 없습니다. 저장해 둔 v{0} 의 설정 (동영상 목록 · 즐겨찾기 포함) 을 되살릴까요?\n(자막 API 키 · 비밀번호는 저장하지 않았으니 다시 넣어 주세요)": "应用为全新安装,没有设置。要恢复保存的 v{0} 设置 (包括视频列表 · 书签) 吗?\n(字幕 API 密钥 · 密码未保存,请重新输入)",
|
||||
"앱을 지우면 다음은 되살릴 수 없습니다:": "删除应用后以下内容无法恢复:",
|
||||
"앱이 켜져 있는 동안 늘 동기화합니다.": "应用运行期间始终同步。",
|
||||
"약 {0} 남음": "约剩 {0}",
|
||||
"양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "双向: 接收端较新的文件不会被覆盖 (-u)。",
|
||||
"어둡게": "调暗",
|
||||
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "在任何地方按下即可显示 JJ_MKVMaker 窗口",
|
||||
|
|
|
|||
|
|
@ -89,6 +89,19 @@ Duration? upscaleEstimate(Map<String, double> secPerMp, AiUpscaler up, double mp
|
|||
return s == null ? null : Duration(seconds: (s * mp).round().clamp(1, 1 << 20));
|
||||
}
|
||||
|
||||
/// 167: sd-cli 진행 → (진행 0~1, 남은 시간). 모델을 읽는 막대 ("192/192 - 84MB/s") 는 올리기 진행이 아니다 -
|
||||
/// 그때는 모름 (null, 움직이는 막대). 올리기는 타일 (n/m) 과 타일 한 장에 걸린 초로 남은 시간을 셈
|
||||
(double?, Duration?) upscaleProgressOf(SdProgress prog) => switch (prog.phase) {
|
||||
'done' => (1.0, Duration.zero),
|
||||
'sample' when prog.steps > 0 => (
|
||||
prog.step / prog.steps,
|
||||
prog.secondsPerStep == null || prog.step == 0
|
||||
? null
|
||||
: Duration(milliseconds: ((prog.steps - prog.step) * prog.secondsPerStep! * 1000).round()),
|
||||
),
|
||||
_ => (null, null),
|
||||
};
|
||||
|
||||
/// 156-2: [result] 가 [input] 의 정확히 4배 크기인지 (그림 머리만 읽음)
|
||||
bool isFourTimes(Uint8List input, Uint8List result) {
|
||||
final a = img.findDecoderForData(input)?.startDecode(input);
|
||||
|
|
@ -118,13 +131,17 @@ class AiUpscaler {
|
|||
}
|
||||
|
||||
/// 4배 결과를 임시 파일로 만든다 (진행: 0~1)
|
||||
Future<String> upscale4x(String input, {void Function(double? p)? onProgress}) async {
|
||||
Future<String> upscale4x(String input, {void Function(double? p)? onProgress, void Function(Duration? left)? onEta}) async {
|
||||
_cancelled = false;
|
||||
final tmp = await Directory.systemTemp.createTemp('jj_up_');
|
||||
final out = p.join(tmp.path, 'up.png');
|
||||
try {
|
||||
await runner(device.exe, sdUpscaleArgs(model: modelPath, input: input, out: out, backend: device.backend, threads: upscaleThreads(device), tileSize: upscaleTileSize(device)),
|
||||
onProgress: (prog) => onProgress?.call(prog.phase == 'done' ? 1 : (prog.steps == 0 ? null : prog.step / prog.steps)),
|
||||
onProgress: (prog) {
|
||||
final (v, left) = upscaleProgressOf(prog);
|
||||
onProgress?.call(v);
|
||||
onEta?.call(left);
|
||||
},
|
||||
onStart: (pr) => _proc = pr);
|
||||
} catch (_) {
|
||||
if (_cancelled) throw ImageAiException(tr('취소했습니다'), cancelled: true);
|
||||
|
|
|
|||
|
|
@ -73,8 +73,15 @@ class TransferJob extends ChangeNotifier {
|
|||
final ms = now.difference(_samples.first.$1).inMilliseconds;
|
||||
if (ms > 0) speed = (_bytes - _samples.first.$2) * 1000 / ms;
|
||||
}
|
||||
// 큰 파일 하나를 보내는 동안에도 진행 막대가 오르게 (파일이 끝날 때만 알리면 0% 에 머문다) - 0.2초마다
|
||||
if (now.difference(_lastTick).inMilliseconds >= 200) {
|
||||
_lastTick = now;
|
||||
_tick();
|
||||
}
|
||||
}
|
||||
|
||||
DateTime _lastTick = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
/// 원본 정리 중 · 지운 빈 폴더 수
|
||||
bool pruning = false;
|
||||
int pruned = 0;
|
||||
|
|
@ -82,10 +89,18 @@ class TransferJob extends ChangeNotifier {
|
|||
// ── 진행 상태 ──
|
||||
int index = 0; // 지금 항목
|
||||
int get total => sources.length;
|
||||
String get currentName => index < sources.length ? p.basename(sources[index]) : '';
|
||||
// 171: 끝난 뒤에도 마지막 항목 이름 (빈 이름으로 " 안의 파일" 이 되던 것)
|
||||
String get currentName => sources.isEmpty ? '' : p.basename(sources[index < sources.length ? index : sources.length - 1]);
|
||||
late final List<int> filesTotal = List.filled(sources.length, 0);
|
||||
late final List<int> filesDone = List.filled(sources.length, 0);
|
||||
|
||||
/// 진행 표시 (한 파일 0% 에 머물던 것): 앱이 직접 복사할 때 항목마다 바이트 크기와 보낸 바이트로 진행률을 센다.
|
||||
/// 같은 드라이브에서 이름 바꾸기로 옮기면 보낸 바이트가 없으므로 파일 수로
|
||||
late final List<int> itemBytes = List.filled(sources.length, 0);
|
||||
int _itemStart = 0;
|
||||
int get _bytesTotal => itemBytes.fold(0, (a, b) => a + b);
|
||||
bool get _byBytes => _bytesTotal > 0 && _bytes > 0;
|
||||
|
||||
/// 지금 파일의 진행률 (rsync -P · robocopy 가 알려 줄 때)
|
||||
double? filePercent;
|
||||
bool counting = true;
|
||||
|
|
@ -114,6 +129,7 @@ class TransferJob extends ChangeNotifier {
|
|||
double get overall {
|
||||
if (finished) return 1;
|
||||
if (total == 0) return 0;
|
||||
if (_byBytes) return (_bytes / _bytesTotal).clamp(0.0, 1.0);
|
||||
final cur = index < total && filesTotal[index] > 0 ? (filesDone[index] / filesTotal[index]).clamp(0.0, 1.0) : 0.0;
|
||||
return ((index + cur) / total).clamp(0.0, 1.0);
|
||||
}
|
||||
|
|
@ -121,7 +137,9 @@ class TransferJob extends ChangeNotifier {
|
|||
/// 아래쪽 진행률: 지금 항목의 파일
|
||||
double get current {
|
||||
if (finished) return 1;
|
||||
if (index >= total || filesTotal[index] == 0) return 0;
|
||||
if (index >= total) return 0;
|
||||
if (_byBytes && itemBytes[index] > 0) return ((_bytes - _itemStart) / itemBytes[index]).clamp(0.0, 1.0);
|
||||
if (filesTotal[index] == 0) return 0;
|
||||
return (filesDone[index] / filesTotal[index]).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
|
|
@ -169,6 +187,12 @@ class TransferJob extends ChangeNotifier {
|
|||
throw FileSystemException('폴더를 자기 안으로 복사 · 이동할 수 없습니다', s);
|
||||
}
|
||||
filesTotal[i] = await countFiles(s);
|
||||
// 앱이 직접 복사할 때만 (robocopy · rsync 는 그 프로그램이 진행을 알려 줌)
|
||||
if (viaWebDav || method == CopyMethod.builtin) {
|
||||
try {
|
||||
itemBytes[i] = await FileOps.totalSize([s]);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
counting = false;
|
||||
_tick();
|
||||
|
|
@ -203,6 +227,7 @@ class TransferJob extends ChangeNotifier {
|
|||
for (index = 0; index < sources.length; index++) {
|
||||
if (cancelled) return;
|
||||
final i = index;
|
||||
_itemStart = _bytes;
|
||||
_ops = FileOps(
|
||||
bandwidthKBps: bandwidthKBps,
|
||||
onFileDone: (src) {
|
||||
|
|
|
|||
|
|
@ -175,6 +175,8 @@ Future<String?> upscaleOne(BuildContext context, AppController c, {required Stri
|
|||
final up = await _prepare(context, c);
|
||||
if (up == null || !context.mounted) return null;
|
||||
final progress = ValueNotifier<double?>(null);
|
||||
// 167: 남은 시간 (타일 한 장에 걸린 초로) - 모르면 null
|
||||
final left = ValueNotifier<Duration?>(null);
|
||||
var cancelled = false;
|
||||
var toPhoto = false;
|
||||
// 관리자: CPU 뿐이라 자동이 만화용을 고르면 사진은 질감이 뭉개질 수 있다 - 알리고 바로 바꿀 수 있게
|
||||
|
|
@ -196,7 +198,17 @@ Future<String?> upscaleOne(BuildContext context, AppController c, {required Stri
|
|||
const SizedBox(height: 10),
|
||||
LinearProgressIndicator(value: v),
|
||||
const SizedBox(height: 4),
|
||||
Text(v == null ? tr('준비하는 중') : '${(v * 100).round()}%', style: const TextStyle(fontSize: 12)),
|
||||
// 167: 모델을 읽는 동안은 숫자 없이 움직이는 막대 (예전엔 읽기 막대가 100% 로 보였다)
|
||||
ValueListenableBuilder<Duration?>(
|
||||
valueListenable: left,
|
||||
builder: (_, d, _) => Text(
|
||||
v == null
|
||||
? tr('준비하는 중')
|
||||
: d == null
|
||||
? '${(v * 100).round()}%'
|
||||
: '${(v * 100).round()}% · ${trf('약 {0} 남음', [AiJobs.durationText(d)])}',
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
if (estimate != null) Text(estimate, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
if (cpuAnime) ...[
|
||||
const SizedBox(height: 10),
|
||||
|
|
@ -229,7 +241,7 @@ Future<String?> upscaleOne(BuildContext context, AppController c, {required Stri
|
|||
Object? error;
|
||||
final watch = Stopwatch()..start();
|
||||
try {
|
||||
four = await up.upscale4x(input, onProgress: (v) => progress.value = v);
|
||||
four = await up.upscale4x(input, onProgress: (v) => progress.value = v, onEta: (d) => left.value = d);
|
||||
await _recordSpeed(c, up, input, watch.elapsed);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:jj_mkvmaker/app/ai_local.dart';
|
|||
import 'package:jj_mkvmaker/app/ai_upscale.dart';
|
||||
import 'package:jj_mkvmaker/app/settings.dart';
|
||||
import 'package:jj_mkvmaker/core/ai_catalog.dart';
|
||||
import 'package:jj_mkvmaker/core/sd_cli.dart';
|
||||
import 'package:jj_mkvmaker/services/image_ai.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
|
|
@ -182,4 +183,12 @@ void main() {
|
|||
expect(b.aiUpSecPerMp, {'cpu|esrgan-anime6b': 442.5});
|
||||
});
|
||||
});
|
||||
|
||||
test('167: 진행 - 모델 읽기 막대는 올리기 진행이 아님 (예전엔 100% 로 보임) · 타일 n/m 과 한 장 초로 남은 시간', () {
|
||||
expect(upscaleProgressOf(parseSdProgress(' |##################################################| 192/192 - 84.37MB/s')!), (null, null));
|
||||
final (v, left) = upscaleProgressOf(parseSdProgress(' |==========> | 5/20 - 4.00s/it')!);
|
||||
expect(v, 0.25);
|
||||
expect(left, const Duration(seconds: 60));
|
||||
expect(upscaleProgressOf(parseSdProgress('[I] 1/1 images saved --- main.cpp:573')!), (1.0, Duration.zero));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,6 +155,21 @@ void main() {
|
|||
expect(File(p.join(dst, 'single.txt')).existsSync(), isFalse);
|
||||
});
|
||||
|
||||
test('진행 표시: 큰 파일 하나도 보낸 바이트로 0% 에서 오른다 · 끝난 뒤에도 항목 이름 (171)', () async {
|
||||
File(p.join(src, 'one.bin')).writeAsBytesSync(List.filled(256 * 1024, 7));
|
||||
final job = TransferJob(sources: [p.join(src, 'one.bin')], dest: dst, move: false, bandwidthKBps: 256);
|
||||
final mid = <double>[];
|
||||
job.addListener(() {
|
||||
if (!job.finished && !job.counting) mid.add(job.overall);
|
||||
});
|
||||
await job.run();
|
||||
expect(job.error, isNull);
|
||||
// 예전에는 파일 수 (0/1) 로 세어 끝날 때까지 0% 였다
|
||||
expect(mid.any((v) => v > 0.2 && v < 1), isTrue, reason: '중간 진행: $mid');
|
||||
expect(job.current, 1);
|
||||
expect(job.currentName, 'one.bin', reason: '끝난 진행 창에 " 안의 파일" 대신 이름');
|
||||
});
|
||||
|
||||
test('현재 방식 속도 제한: 64KB 를 32KB/s 로 → 1.5초 넘게', () async {
|
||||
File(p.join(src, 'big.bin')).writeAsBytesSync(List.filled(64 * 1024, 7));
|
||||
final sw = Stopwatch()..start();
|
||||
|
|
|
|||
Loading…
Reference in New Issue