import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:image/image.dart' as img; import 'package:path/path.dart' as p; import '../core/ai_catalog.dart'; import '../core/sd_cli.dart'; import '../l10n/tr.dart'; import '../services/image_ai.dart'; import 'ai_local.dart'; /// 121: AI 해상도 올리기 (Real-ESRGAN, stable-diffusion.cpp 의 sd-cli -M upscale). /// 모델은 늘 4배로 그리고, 2 · 3배는 그 결과를 줄여 만든다. 원본은 절대 덮어쓰지 않는다. class UpscaleOptions { /// 'auto' · 'photo' · 'anime' final String model; final int scale; /// 'same' (원본과 같은 형식, WebP 등 못 쓰는 형식은 PNG) · 'png' · 'jpg' final String format; final int jpgQuality; /// 저장 폴더 (비어 있으면 원본 옆) final String outDir; const UpscaleOptions({this.model = 'auto', this.scale = 2, this.format = 'same', this.jpgQuality = 92, this.outDir = ''}); } /// 자동 모델: GPU 가 있으면 사진용 (x4plus), CPU 뿐이면 가볍고 빠른 만화용 (CPU 로 x4plus 는 512px 한 장에 20분 - 이 PC 실측) String pickUpscaleModel(String setting, {required bool gpu}) => setting == 'auto' ? (gpu ? 'esrgan-x4plus' : 'esrgan-anime6b') : upscaleModelFor(setting, phone: false); /// 저장 형식 확장자 String upscaleExt(String source, String format) { final e = p.extension(source).toLowerCase().replaceFirst('.', ''); return switch (format) { 'png' => 'png', 'jpg' => 'jpg', _ => const ['jpg', 'jpeg'].contains(e) ? 'jpg' : 'png', }; } /// 저장할 이름: 원본 옆 (또는 [outDir]) 에 "이름_x2.png". 이미 있으면 "이름_x2 (2).png" … (덮어쓰지 않음) String upscaledPath(String source, {required int scale, required String ext, String outDir = ''}) { final dir = outDir.isEmpty ? p.dirname(source) : outDir; final base = '${p.basenameWithoutExtension(source)}_x$scale'; var path = p.join(dir, '$base.$ext'); for (var n = 2; File(path).existsSync(); n++) { path = p.join(dir, '$base ($n).$ext'); } return path; } /// 4배로 올린 그림 [fourX] 를 [scale] 배 크기로 줄이고 형식에 맞게 담는다 (따로 돌려 화면이 멈추지 않게) Future finishUpscale(Uint8List fourX, {required int origW, required int origH, required int scale, required String ext, int quality = 92}) => compute(_finish, (fourX, origW, origH, scale, ext, quality)); Uint8List _finish((Uint8List, int, int, int, String, int) a) { final (bytes, w, h, scale, ext, q) = a; var im = img.decodeImage(bytes); if (im == null) throw const ImageAiException('decode failed'); if (scale != 4 && w > 0 && h > 0) { im = img.copyResize(im, width: w * scale, height: h * scale, interpolation: img.Interpolation.cubic); } return ext == 'jpg' ? img.encodeJpg(im, quality: q) : img.encodePng(im); } /// 그림 한 장 올리기. [input] 은 이 기기의 파일 (ZIP · WebDAV 의 그림은 부르는 쪽이 임시 파일로). /// 결과를 [save] 가 true 면 저장하고 그 경로를, 아니면 임시 파일 경로를 돌려준다 (비교해 보고 저장할 때). class AiUpscaler { AiUpscaler({required this.device, required this.modelPath, this.modelId = ''}); final SdDevice device; final String modelPath; /// 카탈로그 id (자동이 CPU 에서 만화용을 골랐는지 알리려고) final String modelId; Process? _proc; bool _cancelled = false; void cancel() { _cancelled = true; _proc?.kill(); } /// 4배 결과를 임시 파일로 만든다 (진행: 0~1) Future upscale4x(String input, {void Function(double? p)? onProgress}) async { _cancelled = false; final tmp = await Directory.systemTemp.createTemp('jj_up_'); final out = p.join(tmp.path, 'up.png'); try { await runSdCli(device.exe, sdUpscaleArgs(model: modelPath, input: input, out: out, backend: device.backend), onProgress: (prog) => onProgress?.call(prog.phase == 'done' ? 1 : (prog.steps == 0 ? null : prog.step / prog.steps)), onStart: (pr) => _proc = pr); } catch (_) { if (_cancelled) throw ImageAiException(tr('취소했습니다'), cancelled: true); rethrow; } if (_cancelled) throw ImageAiException(tr('취소했습니다'), cancelled: true); if (!File(out).existsSync()) throw ImageAiException(tr('해상도를 올리지 못했습니다')); return out; } /// 올려서 저장까지: 저장한 경로 Future upscaleAndSave(String input, UpscaleOptions o, {String? nameFrom, void Function(double? p)? onProgress}) async { final four = await upscale4x(input, onProgress: onProgress); try { return await saveUpscaled(four, source: nameFrom ?? input, original: input, o: o); } finally { try { await Directory(p.dirname(four)).delete(recursive: true); } catch (_) {} } } } /// 4배 결과 [fourX] 를 설정대로 줄이고 담아 새 파일로 저장한다 (이름은 [source] 기준, 크기는 [original] 기준) Future saveUpscaled(String fourX, {required String source, required String original, required UpscaleOptions o}) async { final orig = img.decodeImage(await File(original).readAsBytes()); final ext = upscaleExt(source, o.format); final bytes = await finishUpscale(await File(fourX).readAsBytes(), origW: orig?.width ?? 0, origH: orig?.height ?? 0, scale: o.scale, ext: ext, quality: o.jpgQuality); final path = upscaledPath(source, scale: o.scale, ext: ext, outDir: o.outDir); await Directory(p.dirname(path)).create(recursive: true); await File(path).writeAsBytes(bytes, flush: true); return path; } /// 156: 쓸 모델 - 자동이면 고른 것을 받아 두지 않았을 때 받아 둔 다른 것을 쓴다 (없으면 null) String? installedUpscaleModel(bool Function(String id) installed, String setting, {required bool gpu}) { final want = pickUpscaleModel(setting, gpu: gpu); if (installed(want)) return want; if (setting != 'auto') return null; for (final id in const ['esrgan-x4plus', 'esrgan-anime6b']) { if (installed(id)) return id; } return null; } /// 해상도 올리기에 쓸 장치 (엔진이 없으면 null) Future pickUpscaleDevice(AiStore store, {String device = 'auto'}) async { final devices = await sdDevices(store); if (devices.isEmpty) return null; SdDevice dev; if (device != 'auto') { dev = devices.where((d) => d.key == device).firstOrNull ?? devices.first; } else { // 해상도 올리기는 그림 만들기와 달리 Vulkan 이 CUDA 보다 빨랐다 (이 PC, x4plus 512→2048: Vulkan 116초 · CUDA 197초 · CPU 1224초) dev = devices.where((d) => d.backend.startsWith('vulkan')).firstOrNull ?? devices.where((d) => d.backend.startsWith('cuda')).firstOrNull ?? devices.first; } return dev; } /// 해상도 올리기에 쓸 장치 · 모델 (없으면 null - 받을 것을 묻는다) Future prepareUpscaler(AiStore store, {required String modelSetting, String device = 'auto', String autoDevice = ''}) async { final dev = await pickUpscaleDevice(store, device: device); if (dev == null) return null; final id = installedUpscaleModel(store.isInstalled, modelSetting, gpu: dev.backend != 'cpu'); if (id == null) return null; return AiUpscaler(device: dev, modelPath: store.pathOf(aiFile(id)), modelId: id); }