설정 A 분야 5: AI 그림 · 해상도 올리기 (AiImageSettings) - 키 27 · aiServices 의 비밀 값 처리 (plainSecrets) 그대로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
059c8cba59
commit
506803ca65
|
|
@ -258,8 +258,8 @@ void main() {
|
|||
await show(t, size, AiImagePage(c: c));
|
||||
await shot(t, '123_ai_local_$tag');
|
||||
c.settings
|
||||
..aiServices = [const AiService(id: 's', name: 'GPU PC (Tailscale)', url: 'http://100.1.2.3:7860', kind: 'a1111')]
|
||||
..aiEngine = 's';
|
||||
..aiImage.services = [const AiService(id: 's', name: 'GPU PC (Tailscale)', url: 'http://100.1.2.3:7860', kind: 'a1111')]
|
||||
..aiImage.engine = 's';
|
||||
await show(t, size, AiImagePage(c: c));
|
||||
await shot(t, '123_ai_service_$tag');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -448,14 +448,14 @@ Future<SdDevice?> pickAiDevice(AppController c, AiStore store, {void Function(St
|
|||
final devices = await sdDevices(store);
|
||||
if (devices.isEmpty) return null;
|
||||
final s = c.settings;
|
||||
if (s.aiDevice != 'auto') return devices.where((d) => d.key == s.aiDevice).firstOrNull ?? devices.first;
|
||||
if (s.aiImage.device != 'auto') return devices.where((d) => d.key == s.aiImage.device).firstOrNull ?? devices.first;
|
||||
if (devices.length == 1) return devices.first;
|
||||
final signature = devices.map((d) => d.key).join(',');
|
||||
if (s.aiAutoDeviceFor == signature) {
|
||||
final known = devices.where((d) => d.key == s.aiAutoDevice).firstOrNull;
|
||||
if (s.aiImage.autoDeviceFor == signature) {
|
||||
final known = devices.where((d) => d.key == s.aiImage.autoDevice).firstOrNull;
|
||||
if (known != null) return known;
|
||||
}
|
||||
final models = store.sd15Paths(taesd: s.aiTaesd);
|
||||
final models = store.sd15Paths(taesd: s.aiImage.taesd);
|
||||
if (models == null) return devices.first;
|
||||
final tmp = await Directory.systemTemp.createTemp('jj_ai_probe_');
|
||||
SdDevice? best;
|
||||
|
|
@ -485,7 +485,7 @@ Future<SdDevice?> pickAiDevice(AppController c, AiStore store, {void Function(St
|
|||
best ??= devices.last;
|
||||
final chosen = best;
|
||||
await c.updateSettings((x) => x
|
||||
..aiAutoDevice = chosen.key
|
||||
..aiAutoDeviceFor = signature);
|
||||
..aiImage.autoDevice = chosen.key
|
||||
..aiImage.autoDeviceFor = signature);
|
||||
return chosen;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import '../core/ftp.dart';
|
|||
import '../core/sftp.dart';
|
||||
import '../core/smb.dart';
|
||||
import '../core/webdav.dart';
|
||||
import '../services/image_ai.dart' show AiService;
|
||||
import '../services/app_shell.dart' show appIconOf;
|
||||
import '../core/remote_idle.dart';
|
||||
import 'settings/records.dart';
|
||||
import 'settings/settings_group.dart';
|
||||
import 'settings/ai_image_settings.dart';
|
||||
import 'settings/copy_settings.dart';
|
||||
import 'settings/tor_settings.dart';
|
||||
import 'settings/network_settings.dart';
|
||||
|
|
@ -24,6 +24,7 @@ import 'settings/explorer_settings.dart';
|
|||
export 'settings/records.dart';
|
||||
export 'settings/settings_group.dart';
|
||||
export 'settings/settings_store.dart';
|
||||
export 'settings/ai_image_settings.dart';
|
||||
export 'settings/copy_settings.dart';
|
||||
export 'settings/tor_settings.dart';
|
||||
export 'settings/network_settings.dart';
|
||||
|
|
@ -346,67 +347,23 @@ class AppSettings {
|
|||
|
||||
// ───────── 123 · 121: AI 그림 · 해상도 올리기 ─────────
|
||||
|
||||
/// 그리는 엔진: 'local' 이 기기 안 (기본) · 아니면 [aiServices] 의 id (사용자가 추가한 서버 · 서비스)
|
||||
String aiEngine = 'local';
|
||||
|
||||
/// 기기 안 처리 장치: 'auto' (잰 결과로 빠른 쪽) 또는 장치 열쇠 (예: 'engine-cuda:cuda0', 'engine-vulkan:cpu')
|
||||
String aiDevice = 'auto';
|
||||
|
||||
/// 'auto' 가 잰 가장 빠른 장치와 그때의 장치 목록 (목록이 바뀌면 다시 잰다)
|
||||
String aiAutoDevice = '';
|
||||
String aiAutoDeviceFor = '';
|
||||
|
||||
/// 빠른 디코더 (TAESD) - 그림이 거의 같고 디코드가 10배쯤 빠르다 (받아 두었을 때)
|
||||
bool aiTaesd = true;
|
||||
int aiWidth = 512;
|
||||
int aiHeight = 512;
|
||||
int aiSteps = 4;
|
||||
double aiCfg = 1;
|
||||
int aiCount = 1;
|
||||
double aiStrength = 0.6;
|
||||
String aiNegative = '';
|
||||
|
||||
/// 159: 마지막 프롬프트와 만든 그림 목록 (최근 것부터, 업데이트 · 다시 열어도 남게)
|
||||
String aiPrompt = '';
|
||||
List<String> aiRecent = [];
|
||||
|
||||
/// 저장 폴더 (비어 있으면 사진 폴더의 JJ_MKVMaker_AI)
|
||||
String aiSaveDir = '';
|
||||
|
||||
/// 사용자가 추가한 그림 서버 · 서비스 (API 키는 안전 저장소에)
|
||||
List<AiService> aiServices = [];
|
||||
|
||||
/// 라이선스 조건에 동의한 받는 파일 (149-④: 한 번 동의하면 다시 묻지 않음)
|
||||
List<String> aiAgreed = [];
|
||||
|
||||
// 121: 해상도 올리기
|
||||
/// 모델 'auto' (GPU 면 사진용, CPU 뿐이면 가볍고 빠른 만화용) · 'photo' · 'anime'
|
||||
String aiUpModel = 'auto';
|
||||
|
||||
/// 176: 해상도 올리기 메모리 (Android CPU 타일 크기) - 'low' 20% · 'normal' 30% (처음 값) · 'high' 45% (기기 메모리 중)
|
||||
String aiUpMemory = 'normal';
|
||||
|
||||
/// 156-2: 해상도 올리기에 걸린 시간 (처리 장치|모델 → 원본 100만 화소당 초) - "한 장 약 n초" 를 미리 알리려고
|
||||
Map<String, double> aiUpSecPerMp = {};
|
||||
|
||||
/// 배율 2 · 3 · 4
|
||||
int aiUpScale = 2;
|
||||
|
||||
/// 관리자 판단 (10/17): 보기 화면의 [품질 올리기] 단추 (그림 → 그림 약하게 + 해상도 올리기). 처음 값: 끔
|
||||
bool aiQualityButton = false;
|
||||
|
||||
/// [품질 올리기] 다듬기 정도 (그림 → 그림 바꾸는 정도): 'weak' 0.15 · 'normal' 0.25 (처음 값) · 'strong' 0.4 (관리자 지시 - 판단은 설정으로)
|
||||
String aiQualityStrength = 'normal';
|
||||
|
||||
/// 저장 형식 'same' (원본과 같게, 못 쓰는 형식은 PNG) · 'png' · 'jpg' · JPG 품질
|
||||
String aiUpFormat = 'same';
|
||||
int aiUpJpgQuality = 92;
|
||||
|
||||
/// 저장 위치 (비어 있으면 원본 옆 "이름_x2")
|
||||
String aiUpDir = '';
|
||||
|
||||
/// 저장한 뒤 'upscaled' 올린 것으로 계속 보기 · 'original' 원본으로
|
||||
String aiUpAfter = 'upscaled';
|
||||
|
||||
|
||||
/// 백그라운드로 실행 (Android): 켜면 ← 로 닫거나 최근 앱에서 밀어도 동기화 · MKV 만들기 · 다운로드를 계속한다 (알림에 표시).
|
||||
|
|
@ -474,8 +431,11 @@ class AppSettings {
|
|||
/// 복사 · 이동 (파일 탐색기) · rsync · 실시간 동기화 (Rsync 화면)
|
||||
final copy = CopySettings();
|
||||
|
||||
/// AI 그림 · 해상도 올리기 (123 · 121)
|
||||
final aiImage = AiImageSettings();
|
||||
|
||||
/// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다
|
||||
List<SettingsGroup> get groups => [explorer, network, tor, copy];
|
||||
List<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage];
|
||||
|
||||
/// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다
|
||||
Map<String, Object?> toJson({bool plainSecrets = false}) {
|
||||
|
|
@ -578,35 +538,6 @@ class AppSettings {
|
|||
'zipComic': zipComic,
|
||||
// 예전 기본값 (만화 보기 켜짐) 은 지시와 반대였으므로 한 번 꺼진 상태 (목록) 로
|
||||
'zipComicV2': true,
|
||||
'aiEngine': aiEngine,
|
||||
'aiDevice': aiDevice,
|
||||
'aiAutoDevice': aiAutoDevice,
|
||||
'aiAutoDeviceFor': aiAutoDeviceFor,
|
||||
'aiTaesd': aiTaesd,
|
||||
'aiWidth': aiWidth,
|
||||
'aiHeight': aiHeight,
|
||||
'aiSteps': aiSteps,
|
||||
'aiCfg': aiCfg,
|
||||
'aiCount': aiCount,
|
||||
'aiStrength': aiStrength,
|
||||
'aiNegative': aiNegative,
|
||||
'aiPrompt': aiPrompt,
|
||||
'aiRecent': aiRecent,
|
||||
'aiSaveDir': aiSaveDir,
|
||||
'aiAgreed': aiAgreed,
|
||||
'aiUpModel': aiUpModel,
|
||||
'aiUpMemory': aiUpMemory,
|
||||
'aiUpSecPerMp': aiUpSecPerMp,
|
||||
'aiUpScale': aiUpScale,
|
||||
'aiQualityButton': aiQualityButton,
|
||||
'aiQualityStrength': aiQualityStrength,
|
||||
'aiUpFormat': aiUpFormat,
|
||||
'aiUpJpgQuality': aiUpJpgQuality,
|
||||
'aiUpDir': aiUpDir,
|
||||
'aiUpAfter': aiUpAfter,
|
||||
'aiServices': [
|
||||
for (final x in aiServices) {...x.toJson(), if (plainSecrets) 'apiKey': x.apiKey},
|
||||
],
|
||||
'webdavServers': [
|
||||
for (final x in webdavServers) {...x.toJson(), if (plainSecrets) 'password': x.password},
|
||||
],
|
||||
|
|
@ -761,41 +692,6 @@ class AppSettings {
|
|||
..allFilesHintShown = j['allFilesHint2'] == true
|
||||
..subtitleScale = ((j['subtitleScale'] as num?)?.toDouble() ?? 1.0).clamp(0.5, 2.5)
|
||||
..zipComic = j['zipComic'] == true && j['zipComicV2'] == true
|
||||
..aiEngine = j['aiEngine'] as String? ?? 'local'
|
||||
..aiDevice = j['aiDevice'] as String? ?? 'auto'
|
||||
..aiAutoDevice = j['aiAutoDevice'] as String? ?? ''
|
||||
..aiAutoDeviceFor = j['aiAutoDeviceFor'] as String? ?? ''
|
||||
..aiTaesd = j['aiTaesd'] != false
|
||||
..aiWidth = ((j['aiWidth'] as num?)?.toInt() ?? 512).clamp(64, 2048)
|
||||
..aiHeight = ((j['aiHeight'] as num?)?.toInt() ?? 512).clamp(64, 2048)
|
||||
..aiSteps = ((j['aiSteps'] as num?)?.toInt() ?? 4).clamp(1, 150)
|
||||
..aiCfg = ((j['aiCfg'] as num?)?.toDouble() ?? 1).clamp(0, 30)
|
||||
..aiCount = ((j['aiCount'] as num?)?.toInt() ?? 1).clamp(1, 100)
|
||||
..aiStrength = ((j['aiStrength'] as num?)?.toDouble() ?? 0.6).clamp(0, 1)
|
||||
..aiNegative = j['aiNegative'] as String? ?? ''
|
||||
..aiPrompt = j['aiPrompt'] as String? ?? ''
|
||||
..aiRecent = [for (final x in (j['aiRecent'] as List?) ?? const []) '$x']
|
||||
..aiSaveDir = j['aiSaveDir'] as String? ?? ''
|
||||
..aiAgreed = [for (final x in (j['aiAgreed'] as List?) ?? const []) '$x']
|
||||
..aiUpModel = const ['photo', 'anime'].contains(j['aiUpModel']) ? j['aiUpModel'] as String : 'auto'
|
||||
..aiUpMemory = const ['low', 'high'].contains(j['aiUpMemory']) ? j['aiUpMemory'] as String : 'normal'
|
||||
..aiUpSecPerMp = {
|
||||
if (j['aiUpSecPerMp'] case final Map m)
|
||||
for (final e in m.entries)
|
||||
if (e.value is num && (e.value as num) > 0) '${e.key}': (e.value as num).toDouble(),
|
||||
}
|
||||
// 1 = 크기는 그대로 선명하게만 (관리자 판단 10/17 - 사용자 질문에 답이 없어 넣되 처음 값은 2배 그대로)
|
||||
..aiUpScale = const [1, 3, 4].contains(j['aiUpScale']) ? j['aiUpScale'] as int : 2
|
||||
..aiQualityButton = j['aiQualityButton'] == true
|
||||
..aiQualityStrength = const ['weak', 'normal', 'strong'].contains(j['aiQualityStrength']) ? j['aiQualityStrength'] as String : 'normal'
|
||||
..aiUpFormat = const ['png', 'jpg'].contains(j['aiUpFormat']) ? j['aiUpFormat'] as String : 'same'
|
||||
..aiUpJpgQuality = ((j['aiUpJpgQuality'] as num?)?.toInt() ?? 92).clamp(50, 100)
|
||||
..aiUpDir = j['aiUpDir'] as String? ?? ''
|
||||
..aiUpAfter = j['aiUpAfter'] == 'original' ? 'original' : 'upscaled'
|
||||
..aiServices = [
|
||||
for (final x in (j['aiServices'] as List?) ?? const [])
|
||||
if (x is Map) AiService.fromJson(x),
|
||||
]
|
||||
..webdavServers = [
|
||||
for (final x in (j['webdavServers'] as List?) ?? const [])
|
||||
if (x is Map) DavServer.fromJson(x),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
|
||||
import '../../services/image_ai.dart';
|
||||
import 'settings_group.dart';
|
||||
|
||||
/// 약속 (설정 A): 환경 설정 > AI 그림 · 해상도 올리기 (123 · 121) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다.
|
||||
/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키)
|
||||
class AiImageSettings implements SettingsGroup {
|
||||
/// 그리는 엔진: 'local' 이 기기 안 (기본) · 아니면 [services] 의 id (사용자가 추가한 서버 · 서비스)
|
||||
String engine = 'local';
|
||||
|
||||
/// 기기 안 처리 장치: 'auto' (잰 결과로 빠른 쪽) 또는 장치 열쇠 (예: 'engine-cuda:cuda0', 'engine-vulkan:cpu')
|
||||
String device = 'auto';
|
||||
|
||||
/// 'auto' 가 잰 가장 빠른 장치와 그때의 장치 목록 (목록이 바뀌면 다시 잰다)
|
||||
String autoDevice = '';
|
||||
|
||||
String autoDeviceFor = '';
|
||||
|
||||
/// 빠른 디코더 (TAESD) - 그림이 거의 같고 디코드가 10배쯤 빠르다 (받아 두었을 때)
|
||||
bool taesd = true;
|
||||
|
||||
int width = 512;
|
||||
|
||||
int height = 512;
|
||||
|
||||
int steps = 4;
|
||||
|
||||
double cfg = 1;
|
||||
|
||||
int count = 1;
|
||||
|
||||
double strength = 0.6;
|
||||
|
||||
String negative = '';
|
||||
|
||||
/// 159: 마지막 프롬프트와 만든 그림 목록 (최근 것부터, 업데이트 · 다시 열어도 남게)
|
||||
String prompt = '';
|
||||
|
||||
List<String> recent = [];
|
||||
|
||||
/// 저장 폴더 (비어 있으면 사진 폴더의 JJ_MKVMaker_AI)
|
||||
String saveDir = '';
|
||||
|
||||
/// 사용자가 추가한 그림 서버 · 서비스 (API 키는 안전 저장소에)
|
||||
List<AiService> services = [];
|
||||
|
||||
/// 라이선스 조건에 동의한 받는 파일 (149-④: 한 번 동의하면 다시 묻지 않음)
|
||||
List<String> agreed = [];
|
||||
|
||||
/// 모델 'auto' (GPU 면 사진용, CPU 뿐이면 가볍고 빠른 만화용) · 'photo' · 'anime'
|
||||
String upModel = 'auto';
|
||||
|
||||
/// 176: 해상도 올리기 메모리 (Android CPU 타일 크기) - 'low' 20% · 'normal' 30% (처음 값) · 'high' 45% (기기 메모리 중)
|
||||
String upMemory = 'normal';
|
||||
|
||||
/// 156-2: 해상도 올리기에 걸린 시간 (처리 장치|모델 → 원본 100만 화소당 초) - "한 장 약 n초" 를 미리 알리려고
|
||||
Map<String, double> upSecPerMp = {};
|
||||
|
||||
/// 배율 2 · 3 · 4
|
||||
int upScale = 2;
|
||||
|
||||
/// 관리자 판단 (10/17): 보기 화면의 [품질 올리기] 단추 (그림 → 그림 약하게 + 해상도 올리기). 처음 값: 끔
|
||||
bool qualityButton = false;
|
||||
|
||||
/// [품질 올리기] 다듬기 정도 (그림 → 그림 바꾸는 정도): 'weak' 0.15 · 'normal' 0.25 (처음 값) · 'strong' 0.4 (관리자 지시 - 판단은 설정으로)
|
||||
String qualityStrength = 'normal';
|
||||
|
||||
/// 저장 형식 'same' (원본과 같게, 못 쓰는 형식은 PNG) · 'png' · 'jpg' · JPG 품질
|
||||
String upFormat = 'same';
|
||||
|
||||
int upJpgQuality = 92;
|
||||
|
||||
/// 저장 위치 (비어 있으면 원본 옆 "이름_x2")
|
||||
String upDir = '';
|
||||
|
||||
/// 저장한 뒤 'upscaled' 올린 것으로 계속 보기 · 'original' 원본으로
|
||||
String upAfter = 'upscaled';
|
||||
|
||||
@override
|
||||
void write(Map<String, Object?> j, {required bool plainSecrets}) {
|
||||
j['aiEngine'] = engine;
|
||||
j['aiDevice'] = device;
|
||||
j['aiAutoDevice'] = autoDevice;
|
||||
j['aiAutoDeviceFor'] = autoDeviceFor;
|
||||
j['aiTaesd'] = taesd;
|
||||
j['aiWidth'] = width;
|
||||
j['aiHeight'] = height;
|
||||
j['aiSteps'] = steps;
|
||||
j['aiCfg'] = cfg;
|
||||
j['aiCount'] = count;
|
||||
j['aiStrength'] = strength;
|
||||
j['aiNegative'] = negative;
|
||||
j['aiPrompt'] = prompt;
|
||||
j['aiRecent'] = recent;
|
||||
j['aiSaveDir'] = saveDir;
|
||||
j['aiServices'] = [
|
||||
for (final x in services) {...x.toJson(), if (plainSecrets) 'apiKey': x.apiKey},
|
||||
];
|
||||
j['aiAgreed'] = agreed;
|
||||
j['aiUpModel'] = upModel;
|
||||
j['aiUpMemory'] = upMemory;
|
||||
j['aiUpSecPerMp'] = upSecPerMp;
|
||||
j['aiUpScale'] = upScale;
|
||||
j['aiQualityButton'] = qualityButton;
|
||||
j['aiQualityStrength'] = qualityStrength;
|
||||
j['aiUpFormat'] = upFormat;
|
||||
j['aiUpJpgQuality'] = upJpgQuality;
|
||||
j['aiUpDir'] = upDir;
|
||||
j['aiUpAfter'] = upAfter;
|
||||
}
|
||||
|
||||
@override
|
||||
void read(Map<String, dynamic> j) {
|
||||
engine = j['aiEngine'] as String? ?? 'local';
|
||||
device = j['aiDevice'] as String? ?? 'auto';
|
||||
autoDevice = j['aiAutoDevice'] as String? ?? '';
|
||||
autoDeviceFor = j['aiAutoDeviceFor'] as String? ?? '';
|
||||
taesd = j['aiTaesd'] != false;
|
||||
width = ((j['aiWidth'] as num?)?.toInt() ?? 512).clamp(64, 2048);
|
||||
height = ((j['aiHeight'] as num?)?.toInt() ?? 512).clamp(64, 2048);
|
||||
steps = ((j['aiSteps'] as num?)?.toInt() ?? 4).clamp(1, 150);
|
||||
cfg = ((j['aiCfg'] as num?)?.toDouble() ?? 1).clamp(0, 30);
|
||||
count = ((j['aiCount'] as num?)?.toInt() ?? 1).clamp(1, 100);
|
||||
strength = ((j['aiStrength'] as num?)?.toDouble() ?? 0.6).clamp(0, 1);
|
||||
negative = j['aiNegative'] as String? ?? '';
|
||||
prompt = j['aiPrompt'] as String? ?? '';
|
||||
recent = [for (final x in (j['aiRecent'] as List?) ?? const []) '$x'];
|
||||
saveDir = j['aiSaveDir'] as String? ?? '';
|
||||
agreed = [for (final x in (j['aiAgreed'] as List?) ?? const []) '$x'];
|
||||
upModel = const ['photo', 'anime'].contains(j['aiUpModel']) ? j['aiUpModel'] as String : 'auto';
|
||||
upMemory = const ['low', 'high'].contains(j['aiUpMemory']) ? j['aiUpMemory'] as String : 'normal';
|
||||
upSecPerMp = {
|
||||
if (j['aiUpSecPerMp'] case final Map m)
|
||||
for (final e in m.entries)
|
||||
if (e.value is num && (e.value as num) > 0) '${e.key}': (e.value as num).toDouble(),
|
||||
};
|
||||
// 1 = 크기는 그대로 선명하게만 (관리자 판단 10/17 - 사용자 질문에 답이 없어 넣되 처음 값은 2배 그대로)
|
||||
upScale = const [1, 3, 4].contains(j['aiUpScale']) ? j['aiUpScale'] as int : 2;
|
||||
qualityButton = j['aiQualityButton'] == true;
|
||||
qualityStrength = const ['weak', 'normal', 'strong'].contains(j['aiQualityStrength']) ? j['aiQualityStrength'] as String : 'normal';
|
||||
upFormat = const ['png', 'jpg'].contains(j['aiUpFormat']) ? j['aiUpFormat'] as String : 'same';
|
||||
upJpgQuality = ((j['aiUpJpgQuality'] as num?)?.toInt() ?? 92).clamp(50, 100);
|
||||
upDir = j['aiUpDir'] as String? ?? '';
|
||||
upAfter = j['aiUpAfter'] == 'original' ? 'original' : 'upscaled';
|
||||
services = [
|
||||
for (final x in (j['aiServices'] as List?) ?? const [])
|
||||
if (x is Map) AiService.fromJson(x),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -67,7 +67,7 @@ class SettingsStore {
|
|||
if (_os(s, e.key).isNotEmpty) e.value: _os(s, e.key),
|
||||
for (final x in s.webdavServers)
|
||||
if (x.password.isNotEmpty) _davKey(x.id): x.password,
|
||||
for (final x in s.aiServices)
|
||||
for (final x in s.aiImage.services)
|
||||
if (x.apiKey.isNotEmpty) _aiKey(x.id): x.apiKey,
|
||||
for (final x in s.sftpServers) ...{
|
||||
if (x.password.isNotEmpty) _sftpPw(x.id): x.password,
|
||||
|
|
@ -116,8 +116,8 @@ class SettingsStore {
|
|||
for (final x in s.webdavServers)
|
||||
x.password.isNotEmpty ? x : x.copyWith(password: stored[_davKey(x.id)] ?? ''),
|
||||
];
|
||||
s.aiServices = [
|
||||
for (final x in s.aiServices) x.apiKey.isNotEmpty ? x : x.copyWith(apiKey: stored[_aiKey(x.id)] ?? ''),
|
||||
s.aiImage.services = [
|
||||
for (final x in s.aiImage.services) x.apiKey.isNotEmpty ? x : x.copyWith(apiKey: stored[_aiKey(x.id)] ?? ''),
|
||||
];
|
||||
s.sftpServers = [
|
||||
for (final x in s.sftpServers)
|
||||
|
|
@ -166,7 +166,7 @@ class SettingsStore {
|
|||
for (final x in s.webdavServers) {
|
||||
if (x.password.isNotEmpty) _filePlain[_davKey(x.id)] = x.password;
|
||||
}
|
||||
for (final x in s.aiServices) {
|
||||
for (final x in s.aiImage.services) {
|
||||
if (x.apiKey.isNotEmpty) _filePlain[_aiKey(x.id)] = x.apiKey;
|
||||
}
|
||||
_fill(s, stored);
|
||||
|
|
|
|||
|
|
@ -59,14 +59,14 @@ String aiSaveDir(String setting) {
|
|||
|
||||
class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
||||
AppController get c => widget.c;
|
||||
late final _prompt = TextEditingController(text: c.settings.aiPrompt);
|
||||
late final _prompt = TextEditingController(text: c.settings.aiImage.prompt);
|
||||
final _promptFocus = FocusNode();
|
||||
late final _negative = TextEditingController(text: c.settings.aiNegative);
|
||||
late final _negative = TextEditingController(text: c.settings.aiImage.negative);
|
||||
final _seed = TextEditingController(text: '-1');
|
||||
late String? _init = widget.initImage;
|
||||
/// 159: 지난 결과를 되살린다 (설정의 목록 + 그림 옆 json 의 시드). 지워진 파일은 빼고 보여 준다
|
||||
late final _results = <GeneratedImage>[
|
||||
for (final path in c.settings.aiRecent)
|
||||
for (final path in c.settings.aiImage.recent)
|
||||
if (File(path).existsSync()) GeneratedImage(path, _seedOf(path)),
|
||||
];
|
||||
String? _error;
|
||||
|
|
@ -83,7 +83,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
|||
/// 목록을 설정에 남긴다 (최근 100장)
|
||||
void _saveRecent() {
|
||||
final paths = [for (final g in _results) g.path].take(100).toList();
|
||||
c.updateSettings((x) => x.aiRecent = paths);
|
||||
c.updateSettings((x) => x.aiImage.recent = paths);
|
||||
}
|
||||
|
||||
/// 150: 엔진 원문 ([자세히] 를 눌러야 보임) · 취소 (회색 한 줄, 오류 아님)
|
||||
|
|
@ -115,7 +115,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
AiService? get _service => c.settings.aiServices.where((x) => x.id == c.settings.aiEngine).firstOrNull;
|
||||
AiService? get _service => c.settings.aiImage.services.where((x) => x.id == c.settings.aiImage.engine).firstOrNull;
|
||||
|
||||
bool get _local => _service == null;
|
||||
|
||||
|
|
@ -135,19 +135,19 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
|||
if (!_modelsReady) return;
|
||||
}
|
||||
await c.updateSettings((x) => x
|
||||
..aiNegative = _negative.text
|
||||
..aiPrompt = _prompt.text);
|
||||
..aiImage.negative = _negative.text
|
||||
..aiImage.prompt = _prompt.text);
|
||||
final req = ImageGenRequest(
|
||||
prompt: _prompt.text.trim(),
|
||||
negative: _negative.text.trim(),
|
||||
width: s.aiWidth,
|
||||
height: s.aiHeight,
|
||||
steps: s.aiSteps,
|
||||
cfg: s.aiCfg,
|
||||
width: s.aiImage.width,
|
||||
height: s.aiImage.height,
|
||||
steps: s.aiImage.steps,
|
||||
cfg: s.aiImage.cfg,
|
||||
seed: int.tryParse(_seed.text.trim()) ?? -1,
|
||||
count: s.aiCount,
|
||||
count: s.aiImage.count,
|
||||
initImage: _init,
|
||||
strength: s.aiStrength,
|
||||
strength: s.aiImage.strength,
|
||||
model: _local ? 'sd15-lcm' : (_service!.model.isEmpty ? _service!.kind : _service!.model),
|
||||
);
|
||||
setState(() {
|
||||
|
|
@ -162,11 +162,11 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
|||
if (_local) {
|
||||
final device = await pickAiDevice(c, store, onStep: (t) => mounted ? setState(() => _step = t) : null);
|
||||
if (device == null) throw ImageAiException(tr('AI 엔진이 없습니다. 환경 설정 > AI 그림에서 받아 주세요.'));
|
||||
engine = LocalSdEngine(device: device, models: store.sd15Paths(taesd: s.aiTaesd)!);
|
||||
engine = LocalSdEngine(device: device, models: store.sd15Paths(taesd: s.aiImage.taesd)!);
|
||||
} else {
|
||||
engine = RemoteImageEngine(_service!);
|
||||
}
|
||||
final made = await AiJobs.instance.run(engine, req, outDir: aiSaveDir(s.aiSaveDir));
|
||||
final made = await AiJobs.instance.run(engine, req, outDir: aiSaveDir(s.aiImage.saveDir));
|
||||
if (!mounted) return;
|
||||
setState(() => _results.insertAll(0, made));
|
||||
_saveRecent();
|
||||
|
|
@ -247,14 +247,14 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(spacing: 12, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||
_drop<String>(tr('크기'), '${s.aiWidth}x${s.aiHeight}', const ['512x512', '512x768', '768x512', '768x768', '1024x1024'],
|
||||
_drop<String>(tr('크기'), '${s.aiImage.width}x${s.aiImage.height}', const ['512x512', '512x768', '768x512', '768x768', '1024x1024'],
|
||||
(v) => c.updateSettings((x) {
|
||||
final wh = v.split('x');
|
||||
x.aiWidth = int.parse(wh[0]);
|
||||
x.aiHeight = int.parse(wh[1]);
|
||||
x.aiImage.width = int.parse(wh[0]);
|
||||
x.aiImage.height = int.parse(wh[1]);
|
||||
})),
|
||||
_drop<int>(tr('단계'), s.aiSteps, const [2, 4, 6, 8, 12, 20, 30], (v) => c.updateSettings((x) => x.aiSteps = v)),
|
||||
_drop<int>(tr('개수'), s.aiCount, const [1, 2, 4, 8, 16], (v) => c.updateSettings((x) => x.aiCount = v)),
|
||||
_drop<int>(tr('단계'), s.aiImage.steps, const [2, 4, 6, 8, 12, 20, 30], (v) => c.updateSettings((x) => x.aiImage.steps = v)),
|
||||
_drop<int>(tr('개수'), s.aiImage.count, const [1, 2, 4, 8, 16], (v) => c.updateSettings((x) => x.aiImage.count = v)),
|
||||
// 141: 이름이 잘리지 않게 짧게 · 넉넉히 (설명은 아래 줄)
|
||||
SizedBox(
|
||||
width: 190,
|
||||
|
|
@ -266,7 +266,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
|||
),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
_initRow(s.aiStrength),
|
||||
_initRow(s.aiImage.strength),
|
||||
const SizedBox(height: 12),
|
||||
if (jobs.busy) ...[
|
||||
LinearProgressIndicator(value: jobs.progress),
|
||||
|
|
@ -299,7 +299,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
|||
]),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (_results.isNotEmpty) Text(trf('만든 그림 · 저장: {0}', [aiSaveDir(s.aiSaveDir)]), style: const TextStyle(fontSize: 12)),
|
||||
if (_results.isNotEmpty) Text(trf('만든 그림 · 저장: {0}', [aiSaveDir(s.aiImage.saveDir)]), style: const TextStyle(fontSize: 12)),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(spacing: 8, runSpacing: 8, children: [for (final g in _results) _thumb(g)]),
|
||||
]),
|
||||
|
|
@ -319,12 +319,12 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
|||
Flexible(
|
||||
child: DropdownButton<String>(
|
||||
isExpanded: true,
|
||||
value: _service == null ? 'local' : s.aiEngine,
|
||||
value: _service == null ? 'local' : s.aiImage.engine,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'local', child: Text(tr('이 기기 (SD1.5 + LCM)'))),
|
||||
for (final x in s.aiServices) DropdownMenuItem(value: x.id, child: Text('☁ ${x.label}')),
|
||||
for (final x in s.aiImage.services) DropdownMenuItem(value: x.id, child: Text('☁ ${x.label}')),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiEngine = v ?? 'local'),
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiImage.engine = v ?? 'local'),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
|
|
@ -357,7 +357,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
|||
value: strength,
|
||||
min: 0.05,
|
||||
max: 1,
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiStrength = v),
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiImage.strength = v),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -83,13 +83,13 @@ class _AiSettingsState extends State<AiSettings> {
|
|||
subtitle: Text(upscaleMemoryLines(mem).join('\n')),
|
||||
isThreeLine: true,
|
||||
trailing: DropdownButton<String>(
|
||||
value: c.settings.aiUpMemory,
|
||||
value: c.settings.aiImage.upMemory,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'low', child: Text(tr('적게 (느림)'))),
|
||||
DropdownMenuItem(value: 'normal', child: Text(tr('보통'))),
|
||||
DropdownMenuItem(value: 'high', child: Text(tr('많이 (빠름)'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiUpMemory = v ?? 'normal'),
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiImage.upMemory = v ?? 'normal'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@ class _AiSettingsState extends State<AiSettings> {
|
|||
/// 받기 전에 라이선스 조건 (있으면) 을 보여 주고 동의를 받는다
|
||||
Future<bool> _agree(AiFile f) async {
|
||||
// 149-④: 한 번 동의했으면 다시 묻지 않는다
|
||||
if (f.terms == null || c.settings.aiAgreed.contains(f.id)) return true;
|
||||
if (f.terms == null || c.settings.aiImage.agreed.contains(f.id)) return true;
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
|
|
@ -127,7 +127,7 @@ class _AiSettingsState extends State<AiSettings> {
|
|||
],
|
||||
),
|
||||
);
|
||||
if (ok == true) await c.updateSettings((x) => x.aiAgreed = {...x.aiAgreed, f.id}.toList());
|
||||
if (ok == true) await c.updateSettings((x) => x.aiImage.agreed = {...x.aiImage.agreed, f.id}.toList());
|
||||
return ok == true;
|
||||
}
|
||||
|
||||
|
|
@ -198,42 +198,42 @@ class _AiSettingsState extends State<AiSettings> {
|
|||
SettingTile(
|
||||
leading: const Icon(Icons.memory),
|
||||
title: Text(tr('처리 장치')),
|
||||
subtitle: Text(s.aiDevice == 'auto'
|
||||
? (s.aiAutoDevice.isEmpty
|
||||
subtitle: Text(s.aiImage.device == 'auto'
|
||||
? (s.aiImage.autoDevice.isEmpty
|
||||
? tr('자동: 처음 그릴 때 짧게 재서 가장 빠른 장치를 고릅니다')
|
||||
: trf('자동: 잰 결과 {0} 가 가장 빠릅니다', [_devices.where((d) => d.key == s.aiAutoDevice).firstOrNull?.label ?? s.aiAutoDevice]))
|
||||
: trf('자동: 잰 결과 {0} 가 가장 빠릅니다', [_devices.where((d) => d.key == s.aiImage.autoDevice).firstOrNull?.label ?? s.aiImage.autoDevice]))
|
||||
: tr('직접 고름')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.aiDevice == 'auto' || _devices.any((d) => d.key == s.aiDevice) ? s.aiDevice : 'auto',
|
||||
value: s.aiImage.device == 'auto' || _devices.any((d) => d.key == s.aiImage.device) ? s.aiImage.device : 'auto',
|
||||
items: [
|
||||
DropdownMenuItem(value: 'auto', child: Text(tr('자동 (가장 빠른 것)'))),
|
||||
for (final d in _devices) DropdownMenuItem(value: d.key, child: Text(d.label)),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x
|
||||
..aiDevice = v ?? 'auto'
|
||||
..aiAutoDeviceFor = ''), // 다시 고르면 다음에 다시 잼
|
||||
..aiImage.device = v ?? 'auto'
|
||||
..aiImage.autoDeviceFor = ''), // 다시 고르면 다음에 다시 잼
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: s.aiTaesd,
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiTaesd = v),
|
||||
value: s.aiImage.taesd,
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiImage.taesd = v),
|
||||
title: Text(tr('빠른 디코더 (TAESD)')),
|
||||
subtitle: Text(tr('그림이 거의 같고 마지막 단계가 10배쯤 빠릅니다 (받아 두었을 때)')),
|
||||
),
|
||||
SettingTile(
|
||||
leading: const Icon(Icons.folder_outlined),
|
||||
title: Text(tr('저장 폴더')),
|
||||
subtitle: Text(aiSaveDir(s.aiSaveDir)),
|
||||
subtitle: Text(aiSaveDir(s.aiImage.saveDir)),
|
||||
trailing: Wrap(spacing: 6, children: [
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
final d = await pickFolder(context, tr('AI 그림 저장 폴더'), aiSaveDir(s.aiSaveDir));
|
||||
if (d != null) await c.updateSettings((x) => x.aiSaveDir = d);
|
||||
final d = await pickFolder(context, tr('AI 그림 저장 폴더'), aiSaveDir(s.aiImage.saveDir));
|
||||
if (d != null) await c.updateSettings((x) => x.aiImage.saveDir = d);
|
||||
},
|
||||
child: Text(tr('바꾸기')),
|
||||
),
|
||||
if (s.aiSaveDir.isNotEmpty)
|
||||
TextButton(onPressed: () => c.updateSettings((x) => x.aiSaveDir = ''), child: Text(tr('처음대로'))),
|
||||
if (s.aiImage.saveDir.isNotEmpty)
|
||||
TextButton(onPressed: () => c.updateSettings((x) => x.aiImage.saveDir = ''), child: Text(tr('처음대로'))),
|
||||
]),
|
||||
),
|
||||
const Divider(),
|
||||
|
|
@ -243,13 +243,13 @@ class _AiSettingsState extends State<AiSettings> {
|
|||
title: Text(tr('해상도 올리기 모델')),
|
||||
subtitle: Text(tr('자동: 그래픽카드 (GPU) 가 있으면 사진용, CPU 뿐이면 가볍고 빠른 만화용. 원본은 덮어쓰지 않고 새 파일로 저장합니다')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.aiUpModel,
|
||||
value: s.aiImage.upModel,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'auto', child: Text(tr('자동'))),
|
||||
DropdownMenuItem(value: 'photo', child: Text(tr('사진'))),
|
||||
DropdownMenuItem(value: 'anime', child: Text(tr('만화 · 그림'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiUpModel = v ?? 'auto'),
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiImage.upModel = v ?? 'auto'),
|
||||
),
|
||||
),
|
||||
// 176: Android CPU - 메모리를 많이 쓸수록 빠르다 (그래픽카드 · Windows 는 엔진이 정함)
|
||||
|
|
@ -258,13 +258,13 @@ class _AiSettingsState extends State<AiSettings> {
|
|||
leading: const Icon(Icons.zoom_out_map),
|
||||
title: Text(tr('배율')),
|
||||
trailing: DropdownButton<int>(
|
||||
value: s.aiUpScale,
|
||||
value: s.aiImage.upScale,
|
||||
// 1배 = 크기는 그대로 두고 모델로 선명하게만 (이름_x1)
|
||||
items: [
|
||||
for (final n in const [1, 2, 3, 4])
|
||||
DropdownMenuItem(value: n, child: Text(n == 1 ? tr('1배 (선명하게)') : trf('{0}배', [n]))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiUpScale = v ?? 2),
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiImage.upScale = v ?? 2),
|
||||
),
|
||||
),
|
||||
// 관리자 판단 (10/17): [품질 올리기] 한 단추 - 처음 값 끔
|
||||
|
|
@ -273,75 +273,75 @@ class _AiSettingsState extends State<AiSettings> {
|
|||
title: Text(tr('품질 올리기 단추 보이기')),
|
||||
subtitle: Text(tr('보기 화면에 [품질 올리기]: AI 그림 (그림 → 그림, 약하게) 으로 다듬은 뒤 해상도 올리기를 이어서. '
|
||||
'새 파일 (이름_q) 로 저장하며 AI 그림 모델이 필요합니다')),
|
||||
trailing: Switch(value: s.aiQualityButton, onChanged: (v) => c.updateSettings((x) => x.aiQualityButton = v)),
|
||||
trailing: Switch(value: s.aiImage.qualityButton, onChanged: (v) => c.updateSettings((x) => x.aiImage.qualityButton = v)),
|
||||
),
|
||||
if (s.aiQualityButton)
|
||||
if (s.aiImage.qualityButton)
|
||||
SettingTile(
|
||||
leading: const Icon(Icons.tune),
|
||||
title: Text(tr('품질 올리기 다듬기 정도')),
|
||||
subtitle: Text(tr('강할수록 많이 다듬지만 원본과 달라질 수 있습니다')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.aiQualityStrength,
|
||||
value: s.aiImage.qualityStrength,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'weak', child: Text(tr('약하게 (0.15)'))),
|
||||
DropdownMenuItem(value: 'normal', child: Text(tr('보통 (0.25, 기본)'))),
|
||||
DropdownMenuItem(value: 'strong', child: Text(tr('강하게 (0.4)'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiQualityStrength = v ?? 'normal'),
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiImage.qualityStrength = v ?? 'normal'),
|
||||
),
|
||||
),
|
||||
SettingTile(
|
||||
leading: const Icon(Icons.image_outlined),
|
||||
title: Text(tr('저장 형식')),
|
||||
subtitle: s.aiUpFormat == 'jpg' ? Text(trf('JPG 품질 {0}', [s.aiUpJpgQuality])) : Text(tr('WebP 등은 PNG 로 저장합니다')),
|
||||
subtitle: s.aiImage.upFormat == 'jpg' ? Text(trf('JPG 품질 {0}', [s.aiImage.upJpgQuality])) : Text(tr('WebP 등은 PNG 로 저장합니다')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.aiUpFormat,
|
||||
value: s.aiImage.upFormat,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'same', child: Text(tr('원본과 같은 형식'))),
|
||||
const DropdownMenuItem(value: 'png', child: Text('PNG')),
|
||||
const DropdownMenuItem(value: 'jpg', child: Text('JPG')),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiUpFormat = v ?? 'same'),
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiImage.upFormat = v ?? 'same'),
|
||||
),
|
||||
),
|
||||
if (s.aiUpFormat == 'jpg')
|
||||
if (s.aiImage.upFormat == 'jpg')
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Slider(
|
||||
value: s.aiUpJpgQuality.toDouble(),
|
||||
value: s.aiImage.upJpgQuality.toDouble(),
|
||||
min: 50,
|
||||
max: 100,
|
||||
divisions: 50,
|
||||
label: '${s.aiUpJpgQuality}',
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiUpJpgQuality = v.round()),
|
||||
label: '${s.aiImage.upJpgQuality}',
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiImage.upJpgQuality = v.round()),
|
||||
),
|
||||
),
|
||||
SettingTile(
|
||||
leading: const Icon(Icons.drive_file_move_outline),
|
||||
title: Text(tr('저장 위치')),
|
||||
subtitle: Text(s.aiUpDir.isEmpty ? tr('원본 옆에 "이름_x2" (ZIP 은 ZIP 옆 "ZIP이름_AI" 폴더)') : s.aiUpDir),
|
||||
subtitle: Text(s.aiImage.upDir.isEmpty ? tr('원본 옆에 "이름_x2" (ZIP 은 ZIP 옆 "ZIP이름_AI" 폴더)') : s.aiImage.upDir),
|
||||
trailing: Wrap(spacing: 6, children: [
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
final d = await pickFolder(context, tr('해상도를 올린 그림 저장 폴더'), s.aiUpDir.isEmpty ? null : s.aiUpDir);
|
||||
if (d != null) await c.updateSettings((x) => x.aiUpDir = d);
|
||||
final d = await pickFolder(context, tr('해상도를 올린 그림 저장 폴더'), s.aiImage.upDir.isEmpty ? null : s.aiImage.upDir);
|
||||
if (d != null) await c.updateSettings((x) => x.aiImage.upDir = d);
|
||||
},
|
||||
child: Text(tr('폴더 고르기')),
|
||||
),
|
||||
if (s.aiUpDir.isNotEmpty)
|
||||
TextButton(onPressed: () => c.updateSettings((x) => x.aiUpDir = ''), child: Text(tr('원본 옆으로'))),
|
||||
if (s.aiImage.upDir.isNotEmpty)
|
||||
TextButton(onPressed: () => c.updateSettings((x) => x.aiImage.upDir = ''), child: Text(tr('원본 옆으로'))),
|
||||
]),
|
||||
),
|
||||
SettingTile(
|
||||
leading: const Icon(Icons.visibility_outlined),
|
||||
title: Text(tr('저장한 뒤')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.aiUpAfter,
|
||||
value: s.aiImage.upAfter,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'upscaled', child: Text(tr('올린 것으로 계속 보기'))),
|
||||
DropdownMenuItem(value: 'original', child: Text(tr('원본으로'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiUpAfter = v ?? 'upscaled'),
|
||||
onChanged: (v) => c.updateSettings((x) => x.aiImage.upAfter = v ?? 'upscaled'),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
|
|
@ -356,7 +356,7 @@ class _AiSettingsState extends State<AiSettings> {
|
|||
label: Text(tr('서비스 추가')),
|
||||
),
|
||||
),
|
||||
for (final x in s.aiServices)
|
||||
for (final x in s.aiImage.services)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: SettingTile(
|
||||
|
|
@ -379,8 +379,8 @@ class _AiSettingsState extends State<AiSettings> {
|
|||
title: trf('"{0}" 을(를) 지울까요?', [x.label]), body: tr('서비스 설정과 저장된 API 키를 지웁니다.'), ok: tr('지우기'));
|
||||
if (!ok) return;
|
||||
await c.updateSettings((y) {
|
||||
y.aiServices = [for (final z in y.aiServices) if (z.id != x.id) z];
|
||||
if (y.aiEngine == x.id) y.aiEngine = 'local';
|
||||
y.aiImage.services = [for (final z in y.aiImage.services) if (z.id != x.id) z];
|
||||
if (y.aiImage.engine == x.id) y.aiImage.engine = 'local';
|
||||
});
|
||||
},
|
||||
),
|
||||
|
|
@ -560,8 +560,8 @@ Future<void> editAiService(BuildContext context, AppController c, {AiService? ol
|
|||
x.dispose();
|
||||
}
|
||||
if (saved == null) return;
|
||||
await c.updateSettings((x) => x.aiServices = [
|
||||
for (final y in x.aiServices) if (y.id != saved.id) y,
|
||||
await c.updateSettings((x) => x.aiImage.services = [
|
||||
for (final y in x.aiImage.services) if (y.id != saved.id) y,
|
||||
saved,
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,11 +26,11 @@ import 'notice.dart';
|
|||
UpscaleOptions upscaleOptionsOf(AppController c, {String? outDir}) {
|
||||
final s = c.settings;
|
||||
return UpscaleOptions(
|
||||
model: s.aiUpModel,
|
||||
scale: s.aiUpScale,
|
||||
format: s.aiUpFormat,
|
||||
jpgQuality: s.aiUpJpgQuality,
|
||||
outDir: outDir ?? s.aiUpDir,
|
||||
model: s.aiImage.upModel,
|
||||
scale: s.aiImage.upScale,
|
||||
format: s.aiImage.upFormat,
|
||||
jpgQuality: s.aiImage.upJpgQuality,
|
||||
outDir: outDir ?? s.aiImage.upDir,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -40,9 +40,9 @@ Future<(String input, String nameFrom, String outDir)?> readerPageFile(AppContro
|
|||
final s = c.settings;
|
||||
if (src is ImageFilesSource) {
|
||||
final path = src.paths[i];
|
||||
if (!isDav(path)) return (path, path, s.aiUpDir);
|
||||
if (!isDav(path)) return (path, path, s.aiImage.upDir);
|
||||
final local = await vLocalCopy(path, src.tempDir);
|
||||
return (local, p.join(aiSaveDir(s.aiSaveDir), vBasename(path)), s.aiUpDir.isEmpty ? aiSaveDir(s.aiSaveDir) : s.aiUpDir);
|
||||
return (local, p.join(aiSaveDir(s.aiImage.saveDir), vBasename(path)), s.aiImage.upDir.isEmpty ? aiSaveDir(s.aiImage.saveDir) : s.aiImage.upDir);
|
||||
}
|
||||
if (src is ZipImagesSource) {
|
||||
final e = src.entries[i];
|
||||
|
|
@ -50,9 +50,9 @@ Future<(String input, String nameFrom, String outDir)?> readerPageFile(AppContro
|
|||
final name = p.basename(e.name);
|
||||
final f = File(p.join(tmp.path, name))..writeAsBytesSync(e.readBytes() ?? const []);
|
||||
final beside = isDav(src.path)
|
||||
? aiSaveDir(s.aiSaveDir)
|
||||
? aiSaveDir(s.aiImage.saveDir)
|
||||
: p.join(p.dirname(src.path), '${p.basenameWithoutExtension(src.path)}_AI');
|
||||
final out = s.aiUpDir.isEmpty ? beside : s.aiUpDir;
|
||||
final out = s.aiImage.upDir.isEmpty ? beside : s.aiImage.upDir;
|
||||
return (f.path, p.join(out, name), out);
|
||||
}
|
||||
return null; // PDF 등은 아직
|
||||
|
|
@ -68,13 +68,13 @@ Future<AiUpscaler?> _prepare(BuildContext context, AppController c) async {
|
|||
final store = AiStore.instance ??= AiStore(Directory.systemTemp.path);
|
||||
Future<AiUpscaler?> ready() =>
|
||||
prepareUpscaler(store,
|
||||
modelSetting: c.settings.aiUpModel, device: c.settings.aiDevice, autoDevice: c.settings.aiAutoDevice, memory: c.settings.aiUpMemory);
|
||||
modelSetting: c.settings.aiImage.upModel, device: c.settings.aiImage.device, autoDevice: c.settings.aiImage.autoDevice, memory: c.settings.aiImage.upMemory);
|
||||
final up = await ready();
|
||||
if (up != null || !context.mounted) return up;
|
||||
// 엔진 (Windows 는 받아야 함) · 모델 중 없는 것
|
||||
final engine = await pickUpscaleDevice(store, device: c.settings.aiDevice) == null && Platform.isWindows ? aiFile('engine-vulkan') : null;
|
||||
final engine = await pickUpscaleDevice(store, device: c.settings.aiImage.device) == null && Platform.isWindows ? aiFile('engine-vulkan') : null;
|
||||
if (!context.mounted) return null;
|
||||
final setting = c.settings.aiUpModel;
|
||||
final setting = c.settings.aiImage.upModel;
|
||||
final anime = aiFile('esrgan-anime6b'), photo = aiFile('esrgan-x4plus');
|
||||
final choices = setting == 'photo'
|
||||
? [photo]
|
||||
|
|
@ -166,7 +166,7 @@ Future<void> _recordSpeed(AppController c, AiUpscaler up, String input, Duration
|
|||
final mp = megapixels(await File(input).readAsBytes());
|
||||
if (mp == null || mp <= 0) return;
|
||||
final v = took.inMilliseconds / 1000 / mp;
|
||||
await c.updateSettings((x) => x.aiUpSecPerMp = {...x.aiUpSecPerMp, upscaleSpeedKey(up): v});
|
||||
await c.updateSettings((x) => x.aiImage.upSecPerMp = {...x.aiImage.upSecPerMp, upscaleSpeedKey(up): v});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +174,7 @@ Future<void> _recordSpeed(AppController c, AiUpscaler up, String input, Duration
|
|||
Future<String?> _estimateText(AppController c, AiUpscaler up, String input) async {
|
||||
try {
|
||||
final mp = megapixels(await File(input).readAsBytes());
|
||||
final d = mp == null ? null : upscaleEstimate(c.settings.aiUpSecPerMp, up, mp);
|
||||
final d = mp == null ? null : upscaleEstimate(c.settings.aiImage.upSecPerMp, up, mp);
|
||||
return d == null ? null : trf('이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)', [AiJobs.durationText(d)]);
|
||||
} catch (_) {
|
||||
return null;
|
||||
|
|
@ -199,7 +199,7 @@ Future<String?> upscaleOne(BuildContext context, AppController c, {required Stri
|
|||
var background = false;
|
||||
var dialogOpen = true;
|
||||
// 관리자: CPU 뿐이라 자동이 만화용을 고르면 사진은 질감이 뭉개질 수 있다 - 알리고 바로 바꿀 수 있게
|
||||
final cpuAnime = c.settings.aiUpModel == 'auto' && up.device.backend == 'cpu' && up.modelId == 'esrgan-anime6b';
|
||||
final cpuAnime = c.settings.aiImage.upModel == 'auto' && up.device.backend == 'cpu' && up.modelId == 'esrgan-anime6b';
|
||||
final estimate = await _estimateText(c, up, input);
|
||||
if (!context.mounted) return null;
|
||||
final nav = Navigator.of(context);
|
||||
|
|
@ -213,7 +213,7 @@ Future<String?> upscaleOne(BuildContext context, AppController c, {required Stri
|
|||
child: ValueListenableBuilder<double?>(
|
||||
valueListenable: progress,
|
||||
builder: (_, v, _) => Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(trf('{0} · {1}배 · {2}', [p.basename(nameFrom), c.settings.aiUpScale, up.device.label])),
|
||||
Text(trf('{0} · {1}배 · {2}', [p.basename(nameFrom), c.settings.aiImage.upScale, up.device.label])),
|
||||
const SizedBox(height: 10),
|
||||
LinearProgressIndicator(value: v),
|
||||
const SizedBox(height: 4),
|
||||
|
|
@ -284,7 +284,7 @@ Future<String?> upscaleOne(BuildContext context, AppController c, {required Stri
|
|||
}
|
||||
if (dialogOpen) nav.pop(); // 진행 창
|
||||
if (toPhoto && context.mounted) {
|
||||
await c.updateSettings((x) => x.aiUpModel = 'photo');
|
||||
await c.updateSettings((x) => x.aiImage.upModel = 'photo');
|
||||
if (!context.mounted) return null;
|
||||
return upscaleOne(context, c, input: input, nameFrom: nameFrom, outDir: outDir);
|
||||
}
|
||||
|
|
@ -369,7 +369,7 @@ Future<void> upscaleAll(BuildContext context, AppController c, ReaderSource src)
|
|||
try {
|
||||
final first = n == 0 ? null : await readerPageFile(c, src, 0);
|
||||
final mp = first == null ? null : megapixels(await File(first.$1).readAsBytes());
|
||||
final d = mp == null ? null : upscaleEstimate(c.settings.aiUpSecPerMp, up, mp);
|
||||
final d = mp == null ? null : upscaleEstimate(c.settings.aiImage.upSecPerMp, up, mp);
|
||||
if (d != null) {
|
||||
estimate = trf('이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)',
|
||||
[AiJobs.durationText(d), AiJobs.durationText(d * n)]);
|
||||
|
|
@ -380,9 +380,9 @@ Future<void> upscaleAll(BuildContext context, AppController c, ReaderSource src)
|
|||
context,
|
||||
title: trf('{0}장 모두 해상도 올리기', [n]),
|
||||
body: [
|
||||
c.settings.aiUpScale == 1
|
||||
c.settings.aiImage.upScale == 1
|
||||
? tr('크기는 그대로 두고 선명하게 해 새 파일로 저장합니다. 원본 · ZIP 은 그대로입니다. 화면을 닫아도 계속하며 작업 알림에 남은 장과 예상 시간이 보입니다.')
|
||||
: trf('{0}배로 올려 새 파일로 저장합니다. 원본 · ZIP 은 그대로입니다. 화면을 닫아도 계속하며 작업 알림에 남은 장과 예상 시간이 보입니다.', [c.settings.aiUpScale]),
|
||||
: trf('{0}배로 올려 새 파일로 저장합니다. 원본 · ZIP 은 그대로입니다. 화면을 닫아도 계속하며 작업 알림에 남은 장과 예상 시간이 보입니다.', [c.settings.aiImage.upScale]),
|
||||
?estimate,
|
||||
].join('\n\n'),
|
||||
ok: tr('시작'),
|
||||
|
|
@ -433,7 +433,7 @@ Future<String?> qualityUp(BuildContext context, AppController c, {required Strin
|
|||
return null;
|
||||
}
|
||||
final store = AiStore.instance ??= AiStore(Directory.systemTemp.path);
|
||||
final models = store.sd15Paths(taesd: c.settings.aiTaesd);
|
||||
final models = store.sd15Paths(taesd: c.settings.aiImage.taesd);
|
||||
if (models == null) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
|
|
@ -506,10 +506,10 @@ Future<String?> qualityUp(BuildContext context, AppController c, {required Strin
|
|||
negative: 'blurry, lowres, noise, jpeg artifacts, deformed',
|
||||
width: w,
|
||||
height: h,
|
||||
steps: c.settings.aiSteps,
|
||||
cfg: c.settings.aiCfg,
|
||||
steps: c.settings.aiImage.steps,
|
||||
cfg: c.settings.aiImage.cfg,
|
||||
initImage: input,
|
||||
strength: qualityStrength(c.settings.aiQualityStrength),
|
||||
strength: qualityStrength(c.settings.aiImage.qualityStrength),
|
||||
),
|
||||
outDir: tmp.path,
|
||||
baseName: 'q',
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ Future<void> clearStoredSecrets(AppController c) => c.updateSettings((s) => s
|
|||
..smbServers = [for (final x in s.smbServers) x.copyWith(password: '')]
|
||||
..ftpServers = [for (final x in s.ftpServers) x.copyWith(password: '')]
|
||||
// AI 서비스 API 키도 (문구 "비밀번호 · API 키" 와 같게 - 예전에는 남았다, 관리자 결정 10/17)
|
||||
..aiServices = [for (final x in s.aiServices) x.copyWith(apiKey: '')]
|
||||
..aiImage.services = [for (final x in s.aiImage.services) x.copyWith(apiKey: '')]
|
||||
..openSubtitlesKey = ''
|
||||
..openSubtitlesPassword = '').then((_) async {
|
||||
// 관리자 판단 (10/21): 다른 판 (새 판) 이 안전 저장소에 넣은 이 판이 모르는 비밀번호 · 키도 (마스터 · 최상은 남김)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Future<String?> upscaleReaderPage(BuildContext context, AppController c, ReaderS
|
|||
final saved = await upscaleOne(context, c, input: file.$1, nameFrom: file.$2, outDir: file.$3);
|
||||
if (saved == null || !context.mounted) return null;
|
||||
final s = src;
|
||||
if (c.settings.aiUpAfter == 'upscaled' && s is ImageFilesSource && !isDav(s.paths[page])) return saved;
|
||||
if (c.settings.aiImage.upAfter == 'upscaled' && s is ImageFilesSource && !isDav(s.paths[page])) return saved;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ List<ReaderBarItem> readerBarItems(BuildContext context, AppController c, Reader
|
|||
one(ReaderAct(const Icon(Icons.hd_outlined), tr('AI 해상도 올리기 (이 장)'), on.upscalePage)),
|
||||
one(ReaderAct(const Icon(Icons.burst_mode_outlined), tr('AI 해상도 올리기 (모든 장)'), () => upscaleAll(context, c, src))),
|
||||
// 관리자 판단 (10/17): [품질 올리기] - 설정으로 켰을 때만
|
||||
if (c.settings.aiQualityButton) one(ReaderAct(const Icon(Icons.auto_fix_high), tr('품질 올리기 (이 장)'), on.qualityPage)),
|
||||
if (c.settings.aiImage.qualityButton) one(ReaderAct(const Icon(Icons.auto_fix_high), tr('품질 올리기 (이 장)'), on.qualityPage)),
|
||||
],
|
||||
// 123: 이 그림을 바탕으로 AI 그림 (그림 → 그림, 원본은 그대로)
|
||||
if (st.aiSource case final path?)
|
||||
|
|
|
|||
|
|
@ -380,13 +380,13 @@ void main() {
|
|||
final secrets = MemorySecretStore();
|
||||
final st = SettingsStore(p.join(dir.path, 'settings.json'), secrets);
|
||||
final s = await st.load();
|
||||
s.aiServices = [const AiService(id: 'x', name: 'n', url: 'http://h', kind: 'a1111', apiKey: 'sk-secret')];
|
||||
s.aiImage.services = [const AiService(id: 'x', name: 'n', url: 'http://h', kind: 'a1111', apiKey: 'sk-secret')];
|
||||
await st.save(s);
|
||||
expect(File(p.join(dir.path, 'settings.json')).readAsStringSync(), isNot(contains('sk-secret')));
|
||||
expect(secrets.values['ai.key.x'], 'sk-secret');
|
||||
final again = await SettingsStore(p.join(dir.path, 'settings.json'), secrets).load();
|
||||
expect(again.aiServices.single.apiKey, 'sk-secret');
|
||||
s.aiServices = [];
|
||||
expect(again.aiImage.services.single.apiKey, 'sk-secret');
|
||||
s.aiImage.services = [];
|
||||
await st.save(s);
|
||||
expect(secrets.values.containsKey('ai.key.x'), isFalse, reason: '지운 서비스의 키는 저장소에서도 지움');
|
||||
});
|
||||
|
|
@ -475,15 +475,15 @@ void main() {
|
|||
|
||||
test('123: AI 설정 처음 값 · 저장 · 예전 설정에 AI 그림 화면을 한 번 넣음', () {
|
||||
final s = AppSettings();
|
||||
expect((s.aiEngine, s.aiDevice, s.aiTaesd, s.aiWidth, s.aiHeight, s.aiSteps, s.aiCfg, s.aiCount),
|
||||
expect((s.aiImage.engine, s.aiImage.device, s.aiImage.taesd, s.aiImage.width, s.aiImage.height, s.aiImage.steps, s.aiImage.cfg, s.aiImage.count),
|
||||
('local', 'auto', true, 512, 512, 4, 1.0, 1));
|
||||
expect(s.components, contains('aiimage'));
|
||||
final b = AppSettings.fromJson((AppSettings()
|
||||
..aiWidth = 768
|
||||
..aiSteps = 8
|
||||
..aiTaesd = false)
|
||||
..aiImage.width = 768
|
||||
..aiImage.steps = 8
|
||||
..aiImage.taesd = false)
|
||||
.toJson());
|
||||
expect((b.aiWidth, b.aiSteps, b.aiTaesd), (768, 8, false));
|
||||
expect((b.aiImage.width, b.aiImage.steps, b.aiImage.taesd), (768, 8, false));
|
||||
final old = AppSettings.fromJson({'components': ['mkv', 'browser']});
|
||||
expect(old.components, contains('aiimage'));
|
||||
expect(old.migrated, contains('aiImage'));
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ void main() {
|
|||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
if (service != null) {
|
||||
c.settings
|
||||
..aiServices = [service]
|
||||
..aiEngine = service.id;
|
||||
..aiImage.services = [service]
|
||||
..aiImage.engine = service.id;
|
||||
}
|
||||
await t.pumpWidget(MaterialApp(home: AiImagePage(c: c)));
|
||||
await t.pump();
|
||||
|
|
|
|||
|
|
@ -22,16 +22,16 @@ void main() {
|
|||
tearDown(() => tmp.deleteSync(recursive: true));
|
||||
|
||||
test('설정: 배율 1 을 받아들이고 처음 값은 2 · 품질 올리기 단추는 처음 끔 · 저장', () {
|
||||
expect(AppSettings().aiUpScale, 2);
|
||||
expect(AppSettings.fromJson({'aiUpScale': 1}).aiUpScale, 1);
|
||||
expect(AppSettings.fromJson({'aiUpScale': 7}).aiUpScale, 2);
|
||||
expect(AppSettings().aiQualityButton, isFalse);
|
||||
expect(AppSettings.fromJson((AppSettings()..aiQualityButton = true).toJson()).aiQualityButton, isTrue);
|
||||
expect(AppSettings().aiImage.upScale, 2);
|
||||
expect(AppSettings.fromJson({'aiUpScale': 1}).aiImage.upScale, 1);
|
||||
expect(AppSettings.fromJson({'aiUpScale': 7}).aiImage.upScale, 2);
|
||||
expect(AppSettings().aiImage.qualityButton, isFalse);
|
||||
expect(AppSettings.fromJson((AppSettings()..aiImage.qualityButton = true).toJson()).aiImage.qualityButton, isTrue);
|
||||
// 다듬기 정도: 약하게 0.15 · 보통 0.25 (처음) · 강하게 0.4, 모르는 값은 보통
|
||||
expect(AppSettings().aiQualityStrength, 'normal');
|
||||
expect(AppSettings().aiImage.qualityStrength, 'normal');
|
||||
expect([for (final s in ['weak', 'normal', 'strong']) qualityStrength(s)], [0.15, 0.25, 0.4]);
|
||||
expect(AppSettings.fromJson({'aiQualityStrength': 'x'}).aiQualityStrength, 'normal');
|
||||
expect(AppSettings.fromJson((AppSettings()..aiQualityStrength = 'strong').toJson()).aiQualityStrength, 'strong');
|
||||
expect(AppSettings.fromJson({'aiQualityStrength': 'x'}).aiImage.qualityStrength, 'normal');
|
||||
expect(AppSettings.fromJson((AppSettings()..aiImage.qualityStrength = 'strong').toJson()).aiImage.qualityStrength, 'strong');
|
||||
});
|
||||
|
||||
test('1배: 4배 결과를 원본 크기로 줄인다 (선명하게만)', () async {
|
||||
|
|
|
|||
|
|
@ -75,15 +75,15 @@ void main() {
|
|||
|
||||
test('121: 설정 처음 값 · 저장', () {
|
||||
final s = AppSettings();
|
||||
expect((s.aiUpModel, s.aiUpScale, s.aiUpFormat, s.aiUpDir, s.aiUpAfter), ('auto', 2, 'same', '', 'upscaled'));
|
||||
expect((s.aiImage.upModel, s.aiImage.upScale, s.aiImage.upFormat, s.aiImage.upDir, s.aiImage.upAfter), ('auto', 2, 'same', '', 'upscaled'));
|
||||
final b = AppSettings.fromJson((AppSettings()
|
||||
..aiUpModel = 'photo'
|
||||
..aiUpScale = 4
|
||||
..aiUpFormat = 'jpg'
|
||||
..aiUpJpgQuality = 80
|
||||
..aiUpAfter = 'original')
|
||||
..aiImage.upModel = 'photo'
|
||||
..aiImage.upScale = 4
|
||||
..aiImage.upFormat = 'jpg'
|
||||
..aiImage.upJpgQuality = 80
|
||||
..aiImage.upAfter = 'original')
|
||||
.toJson());
|
||||
expect((b.aiUpModel, b.aiUpScale, b.aiUpFormat, b.aiUpJpgQuality, b.aiUpAfter), ('photo', 4, 'jpg', 80, 'original'));
|
||||
expect((b.aiImage.upModel, b.aiImage.upScale, b.aiImage.upFormat, b.aiImage.upJpgQuality, b.aiImage.upAfter), ('photo', 4, 'jpg', 80, 'original'));
|
||||
});
|
||||
|
||||
// 진짜 엔진 (JJ_TEST_SD_DIR: bin_vulkan/sd-cli.exe · models/RealESRGAN_x4plus_anime_6B.pth)
|
||||
|
|
@ -127,11 +127,11 @@ void main() {
|
|||
|
||||
test('159: 프롬프트 · 만든 그림 목록이 설정에 남는다', () {
|
||||
final b = AppSettings.fromJson((AppSettings()
|
||||
..aiPrompt = 'a cat'
|
||||
..aiRecent = ['C:/a/ai_1.png', 'C:/a/ai_2.png'])
|
||||
..aiImage.prompt = 'a cat'
|
||||
..aiImage.recent = ['C:/a/ai_1.png', 'C:/a/ai_2.png'])
|
||||
.toJson());
|
||||
expect(b.aiPrompt, 'a cat');
|
||||
expect(b.aiRecent, ['C:/a/ai_1.png', 'C:/a/ai_2.png']);
|
||||
expect(b.aiImage.prompt, 'a cat');
|
||||
expect(b.aiImage.recent, ['C:/a/ai_1.png', 'C:/a/ai_2.png']);
|
||||
});
|
||||
|
||||
group('156-2: 가짜 AI 결과 막기', () {
|
||||
|
|
@ -186,8 +186,8 @@ void main() {
|
|||
expect(upscaleEstimate({}, up, 0.2), isNull);
|
||||
// S10 실측: 512×384 (0.197MP) 에 약 87초 → 100만 화소당 약 442초
|
||||
expect(upscaleEstimate({'cpu|esrgan-anime6b': 442}, up, 0.196608)!.inSeconds, 87);
|
||||
final b = AppSettings.fromJson((AppSettings()..aiUpSecPerMp = {'cpu|esrgan-anime6b': 442.5}).toJson());
|
||||
expect(b.aiUpSecPerMp, {'cpu|esrgan-anime6b': 442.5});
|
||||
final b = AppSettings.fromJson((AppSettings()..aiImage.upSecPerMp = {'cpu|esrgan-anime6b': 442.5}).toJson());
|
||||
expect(b.aiImage.upSecPerMp, {'cpu|esrgan-anime6b': 442.5});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ void main() {
|
|||
..openSubtitlesPassword = 'op'
|
||||
..webdavServers = [const DavServer(id: 'd1', name: 'n', url: 'https://x', user: 'u', password: 'dp')]
|
||||
..sftpServers = [const SftpServer(id: 's1', name: 'n', host: 'h', user: 'u', password: 'sp', privateKey: 'PRIVATE KEY', keyPassphrase: 'kp')]
|
||||
..aiServices = [const AiService(id: 'a1', name: 'n', url: 'https://ai', kind: 'openai', apiKey: 'ak')]);
|
||||
..aiImage.services = [const AiService(id: 'a1', name: 'n', url: 'https://ai', kind: 'openai', apiKey: 'ak')]);
|
||||
await store.save(c.settings);
|
||||
bool secret(String k) => k.startsWith('os.') || k.startsWith('dav.pw.') || k.startsWith('sftp.') || k.startsWith('ai.');
|
||||
expect(secrets.values.keys.where(secret).length, 7, reason: '넣은 비밀 값이 저장소에 있다 (os 2 · dav 1 · sftp 3 · ai 1)');
|
||||
|
|
@ -295,7 +295,7 @@ void main() {
|
|||
await store.save(c.settings);
|
||||
expect(secrets.values.keys.where(secret), isEmpty);
|
||||
// 목록 · 아이디 · 믿은 서버 키는 남김
|
||||
expect((c.settings.webdavServers.single.user, c.settings.sftpServers.single.user, c.settings.aiServices.single.url), ('u', 'u', 'https://ai'));
|
||||
expect((c.settings.webdavServers.single.user, c.settings.sftpServers.single.user, c.settings.aiImage.services.single.url), ('u', 'u', 'https://ai'));
|
||||
});
|
||||
|
||||
test('관리자 판단 (10/21): "앱 설정 전체" 는 이 판이 모르는 설정 키도 지우고, 다른 판의 비밀 값은 "저장된 비밀번호" 를 골랐을 때만', () async {
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ void main() {
|
|||
..sftpServers = [const SftpServer(id: 's1', name: 'n', host: 'h', user: 'u', password: 'sp', privateKey: 'KEY', keyPassphrase: 'kp')]
|
||||
..smbServers = [const SmbServer(id: 'm1', name: 'n', host: 'h', share: 'v', password: 'mp')]
|
||||
..ftpServers = [const FtpServer(id: 'f1', name: 'n', host: 'h', password: 'fp')]
|
||||
..aiServices = [const AiService(id: 'a1', name: 'n', url: 'https://ai', kind: 'openai', apiKey: 'ak')];
|
||||
..aiImage.services = [const AiService(id: 'a1', name: 'n', url: 'https://ai', kind: 'openai', apiKey: 'ak')];
|
||||
await store.save(s);
|
||||
final made = secrets.values.keys.where((k) => !k.startsWith('sec.')).toList()..sort();
|
||||
expect(made, hasLength(11), reason: '$made'); // os 3 · dav.servers · dav.pw · sftp.pw · key · kp · smb.pw · ftp.pw · ai.key
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ void main() {
|
|||
});
|
||||
|
||||
test('176: 해상도 올리기 메모리 설정 - 적게 20% · 보통 30% (처음 값) · 많이 45%, 저장', () {
|
||||
expect(AppSettings().aiUpMemory, 'normal');
|
||||
expect(AppSettings.fromJson((AppSettings()..aiUpMemory = 'high').toJson()).aiUpMemory, 'high');
|
||||
expect(AppSettings.fromJson({'aiUpMemory': 'x'}).aiUpMemory, 'normal');
|
||||
expect(AppSettings().aiImage.upMemory, 'normal');
|
||||
expect(AppSettings.fromJson((AppSettings()..aiImage.upMemory = 'high').toJson()).aiImage.upMemory, 'high');
|
||||
expect(AppSettings.fromJson({'aiUpMemory': 'x'}).aiImage.upMemory, 'normal');
|
||||
// S10 (약 15GB): 적게 320 · 보통 384 · 많이 512
|
||||
expect([for (final m in ['low', 'normal', 'high']) androidUpscaleTile(memTotalMb: 15000, share: upscaleMemoryShare(m))], [320, 384, 512]);
|
||||
expect(upscaleTileMemoryMb(512), 6600);
|
||||
|
|
|
|||
Loading…
Reference in New Issue