ARMv8.0 기기용 AI 엔진 · 신호별 문구 · 받기 전 검사 · APK 네이티브 목록 (195)
관리자 지시 (정식 판 문제, P11 = Snapdragon 662 에서 해상도 올리기가 "코드 -4" 로 바로 멈춤): sd-cli 를 armv8.2 (dotprod · fp16 · i8mm) 로만
빌드해 dotprod · fp16 이 없는 CPU 에서 SIGILL 로 죽었다.
- tool/build_sdcpp_android.sh [v82|v80]: v80 (armv8-a) 판 libsdcli_v80.so 를 더한다 (APK +15MB).
앱은 /proc/cpuinfo 의 모든 코어 Features 에 asimddp · fphp 가 있을 때만 빠른 판, 아니면 (모름 포함) v80 판.
- 엔진이 신호로 죽으면: SIGILL → "이 기기의 CPU 가 이 AI 엔진을 지원하지 않습니다", SIGKILL → 메모리, 그 밖은 코드만 (메모리 · 파일 탓이라고 하지 않음)
- 모델을 받기 전에 엔진을 --help 로 한 번 실행해 보고, 못 돌면 받지 않고 이유를 알린다. 결과는 작업 기록에 한 줄 ("AI 엔진 확인: 판 · 결과")
- APK 에 넣을 third_party 실행 파일을 android/app/third_party_libs.txt 로 정한다 (gradle 은 목록 밖을 빼고, check_apk.ps1 이 목록과 APK 를 비교)
- 기기 확인 (a4): P11 "libsdcli_v80.so · 통과", 해상도 올리기 약 5분 30초 · 저장. 노트 10+ (dotprod 있음) 는 빠른 판 그대로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix_mkv
parent
7df43a750e
commit
850b148d9e
|
|
@ -63,6 +63,13 @@ android {
|
|||
sourceSets["main"].jniLibs.srcDir("../../third_party/rsync/android")
|
||||
// 123 · 121: stable-diffusion.cpp 의 sd-cli (libsdcli.so 라는 이름의 실행 파일): tool/build_sdcpp_android.sh
|
||||
sourceSets["main"].jniLibs.srcDir("../../third_party/sdcpp/android")
|
||||
// 195: third_party 의 실행 파일은 third_party_libs.txt 에 적은 것만 (공유 폴더에 다른 판이 있어도 섞이지 않게)
|
||||
val thirdPartyLibs = file("third_party_libs.txt").readLines().map { it.trim() }.filter { it.isNotEmpty() && !it.startsWith("#") }.toSet()
|
||||
listOf("../../third_party/rsync/android", "../../third_party/sdcpp/android").forEach { dir ->
|
||||
fileTree(dir).matching { include("**/*.so") }.forEach { so ->
|
||||
if (so.name !in thirdPartyLibs) packaging.jniLibs.excludes += "**/${so.name}"
|
||||
}
|
||||
}
|
||||
|
||||
// ffmpeg · whisper 등 네이티브 라이브러리를 압축하지 않고 그대로 (설치 후 바로 로드)
|
||||
packaging {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
# APK 에 넣을 third_party 의 네이티브 실행 파일 (arm64-v8a). 이 목록이 기준이다 (관리자 지시, 195):
|
||||
# third_party 는 git 밖 (공유 폴더) 이라 빌드하는 순간 거기 있는 파일이 APK 에 들어갔다 - 쓰지 않는 파일이 섞이지 않게.
|
||||
# 여기 없는 파일은 android/app/build.gradle.kts 가 APK 에서 빼고, tool/check_apk.ps1 이 목록과 APK 를 비교한다.
|
||||
# 파일을 더하거나 빼면 그 파일을 쓰는 코드와 함께 이 목록을 고친다.
|
||||
librsync.so
|
||||
libsdcli.so
|
||||
libsdcli_v80.so
|
||||
|
|
@ -77,8 +77,10 @@
|
|||
"AI 번역": "AI translation",
|
||||
"AI 설정 · 모델 받기 · 서비스 추가": "AI settings · download models · add services",
|
||||
"AI 엔진 ({0}) 도 함께 받습니다.": "The AI engine ({0}) will be downloaded too.",
|
||||
"AI 엔진 확인: {0} · {1}": "AI engine check: {0} · {1}",
|
||||
"AI 엔진이 그림을 그리지 못했습니다 (처리 장치를 바꿔 보세요)": "The AI engine could not draw the image (try another processing device)",
|
||||
"AI 엔진이 멈췄습니다 (코드 {0}). 메모리가 모자라거나 파일이 깨졌을 수 있습니다": "The AI engine stopped (code {0}). Memory may be low or a file may be damaged",
|
||||
"AI 엔진이 멈췄습니다 (코드 {0}). 엔진 기록은 [자세히] 에 있습니다": "The AI engine stopped (code {0}). The engine log is under [Details]",
|
||||
"AI 엔진이 실패했습니다 ({0}): {1}": "The AI engine failed ({0}): {1}",
|
||||
"AI 엔진이 없습니다. 환경 설정 > AI 그림에서 받아 주세요.": "No AI engine. Download it in Settings > AI images.",
|
||||
"AI 엔진이 해상도를 올리거나 그리지 못했습니다 (처리 장치를 바꿔 보세요)": "The AI engine could not upscale or draw (try another processing device)",
|
||||
|
|
@ -820,6 +822,7 @@
|
|||
"멈춤 (이번 실행 동안)": "Pause (this session)",
|
||||
"멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "Paused (this session) · press the start button to sync again",
|
||||
"멈춤 · {0}": "Stopped · {0}",
|
||||
"메모리가 모자라 Android 가 AI 엔진을 끝냈습니다. 다른 앱을 닫거나 환경 설정의 해상도 올리기 메모리를 \"적게\" 로 해 보세요": "Android stopped the AI engine because memory ran out. Close other apps or set the upscaling memory in Settings to \"Low\"",
|
||||
"모니터링": "Monitor",
|
||||
"모니터링의 [실행]": "[Run] in Monitoring",
|
||||
"모델 (선택)": "Model (optional)",
|
||||
|
|
@ -1173,6 +1176,7 @@
|
|||
"실행 · 종료": "Run · quit",
|
||||
"실행 취소": "Undo",
|
||||
"실행한 rsync 가 없습니다. Rsync 화면에서 → · ← · ⇄ 로 실행하면 여기에 자동으로 등록됩니다.": "No rsync runs yet. Runs started with → · ← · ⇄ on the Rsync screen are added here automatically.",
|
||||
"실행해 보지 못함 ({0})": "Could not run it ({0})",
|
||||
"싱크": "Sync",
|
||||
"쓰레드 확인 (AI · 변환 작업이 멈출 때까지)": "Thread check (until AI · conversion jobs stop)",
|
||||
"쓸 수 없는 이름입니다": "This name cannot be used",
|
||||
|
|
@ -1413,8 +1417,10 @@
|
|||
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "About {0} per image on this device (based on last time)",
|
||||
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "About {0} per image, {1} in total on this device (based on last time)",
|
||||
"이 기기에서는 AI 번역을 쓸 수 없습니다.": "AI translation is not available on this device.",
|
||||
"이 기기에서는 AI 엔진을 찾지 못했습니다. 앱을 다시 설치해 보세요": "The AI engine was not found on this device. Try reinstalling the app",
|
||||
"이 기기에서는 쓸 수 없는 컴포넌트입니다": "This component is not available on this device",
|
||||
"이 기기용 파일 없음": "No file for this device",
|
||||
"이 기기의 CPU 가 이 AI 엔진을 지원하지 않습니다": "This device's CPU does not support this AI engine",
|
||||
"이 기기의 안전 저장소를 쓸 수 없어 비밀번호를 저장하지 못했습니다": "Couldn't save the password because this device's secure storage is unavailable",
|
||||
"이 기기의 폴더": "Folder on this device",
|
||||
"이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)": "Cannot register this hotkey (another program may be using it)",
|
||||
|
|
@ -1861,6 +1867,7 @@
|
|||
"터키어": "Turkish",
|
||||
"토": "Sat",
|
||||
"토렌트 · 마그넷 다운로드": "Torrent · magnet download",
|
||||
"통과": "Passed",
|
||||
"트랙 #{0} ({1}){2}": "Track #{0} ({1}){2}",
|
||||
"트랙 {0}": "Track {0}",
|
||||
"트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "Reopen with the tray icon or the hotkey below",
|
||||
|
|
|
|||
|
|
@ -77,8 +77,10 @@
|
|||
"AI 번역": "AI 翻訳",
|
||||
"AI 설정 · 모델 받기 · 서비스 추가": "AI 設定・モデルのダウンロード・サービス追加",
|
||||
"AI 엔진 ({0}) 도 함께 받습니다.": "AI エンジン ({0}) も一緒にダウンロードします。",
|
||||
"AI 엔진 확인: {0} · {1}": "AI エンジンの確認: {0} · {1}",
|
||||
"AI 엔진이 그림을 그리지 못했습니다 (처리 장치를 바꿔 보세요)": "AI エンジンが画像を描けませんでした (処理装置を変えてみてください)",
|
||||
"AI 엔진이 멈췄습니다 (코드 {0}). 메모리가 모자라거나 파일이 깨졌을 수 있습니다": "AI エンジンが停止しました (コード {0})。メモリ不足かファイルの破損の可能性があります",
|
||||
"AI 엔진이 멈췄습니다 (코드 {0}). 엔진 기록은 [자세히] 에 있습니다": "AI エンジンが停止しました (コード {0})。エンジンの記録は [詳細] にあります",
|
||||
"AI 엔진이 실패했습니다 ({0}): {1}": "AI エンジンが失敗しました ({0}): {1}",
|
||||
"AI 엔진이 없습니다. 환경 설정 > AI 그림에서 받아 주세요.": "AI エンジンがありません。環境設定 > AI 画像でダウンロードしてください。",
|
||||
"AI 엔진이 해상도를 올리거나 그리지 못했습니다 (처리 장치를 바꿔 보세요)": "AI エンジンが解像度アップまたは描画をできませんでした (処理装置を変えてみてください)",
|
||||
|
|
@ -820,6 +822,7 @@
|
|||
"멈춤 (이번 실행 동안)": "停止 (今回の実行中)",
|
||||
"멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "停止中 (今回の実行中) · 開始ボタンで再び同期します",
|
||||
"멈춤 · {0}": "停止 · {0}",
|
||||
"메모리가 모자라 Android 가 AI 엔진을 끝냈습니다. 다른 앱을 닫거나 환경 설정의 해상도 올리기 메모리를 \"적게\" 로 해 보세요": "メモリ不足のため Android が AI エンジンを終了しました。ほかのアプリを閉じるか、設定の解像度アップのメモリを「少なめ」にしてください",
|
||||
"모니터링": "モニタリング",
|
||||
"모니터링의 [실행]": "モニタリングの [実行]",
|
||||
"모델 (선택)": "モデル (任意)",
|
||||
|
|
@ -1173,6 +1176,7 @@
|
|||
"실행 · 종료": "実行・終了",
|
||||
"실행 취소": "元に戻す",
|
||||
"실행한 rsync 가 없습니다. Rsync 화면에서 → · ← · ⇄ 로 실행하면 여기에 자동으로 등록됩니다.": "実行した rsync はありません。Rsync 画面で → · ← · ⇄ で実行すると自動でここに登録されます。",
|
||||
"실행해 보지 못함 ({0})": "実行できませんでした ({0})",
|
||||
"싱크": "同期",
|
||||
"쓰레드 확인 (AI · 변환 작업이 멈출 때까지)": "スレッド確認(AI・変換作業が止まるまで)",
|
||||
"쓸 수 없는 이름입니다": "使用できない名前です",
|
||||
|
|
@ -1413,8 +1417,10 @@
|
|||
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0} (前回の所要時間から)",
|
||||
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0}・全部で約 {1} (前回の所要時間から)",
|
||||
"이 기기에서는 AI 번역을 쓸 수 없습니다.": "この端末では AI 翻訳を使えません。",
|
||||
"이 기기에서는 AI 엔진을 찾지 못했습니다. 앱을 다시 설치해 보세요": "この端末で AI エンジンが見つかりませんでした。アプリを再インストールしてください",
|
||||
"이 기기에서는 쓸 수 없는 컴포넌트입니다": "この端末では使えないコンポーネントです",
|
||||
"이 기기용 파일 없음": "このデバイス用のファイルなし",
|
||||
"이 기기의 CPU 가 이 AI 엔진을 지원하지 않습니다": "この端末の CPU はこの AI エンジンに対応していません",
|
||||
"이 기기의 안전 저장소를 쓸 수 없어 비밀번호를 저장하지 못했습니다": "この端末の安全な保存領域を使えないため、パスワードを保存できませんでした",
|
||||
"이 기기의 폴더": "この端末のフォルダー",
|
||||
"이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)": "このショートカットを登録できません(他のプログラムが使用中の可能性があります)",
|
||||
|
|
@ -1861,6 +1867,7 @@
|
|||
"터키어": "トルコ語",
|
||||
"토": "土",
|
||||
"토렌트 · 마그넷 다운로드": "トレント・マグネットのダウンロード",
|
||||
"통과": "合格",
|
||||
"트랙 #{0} ({1}){2}": "トラック #{0}({1}){2}",
|
||||
"트랙 {0}": "トラック {0}",
|
||||
"트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "トレイアイコンを押すか、下のショートカットで再表示します",
|
||||
|
|
|
|||
|
|
@ -77,8 +77,10 @@
|
|||
"AI 번역": "AI 翻译",
|
||||
"AI 설정 · 모델 받기 · 서비스 추가": "AI 设置 · 下载模型 · 添加服务",
|
||||
"AI 엔진 ({0}) 도 함께 받습니다.": "也会一并下载 AI 引擎 ({0})。",
|
||||
"AI 엔진 확인: {0} · {1}": "AI 引擎检查:{0} · {1}",
|
||||
"AI 엔진이 그림을 그리지 못했습니다 (처리 장치를 바꿔 보세요)": "AI 引擎未能生成图片 (请尝试更换处理设备)",
|
||||
"AI 엔진이 멈췄습니다 (코드 {0}). 메모리가 모자라거나 파일이 깨졌을 수 있습니다": "AI 引擎已停止 (代码 {0})。可能是内存不足或文件损坏",
|
||||
"AI 엔진이 멈췄습니다 (코드 {0}). 엔진 기록은 [자세히] 에 있습니다": "AI 引擎已停止(代码 {0})。引擎记录在 [详细] 中",
|
||||
"AI 엔진이 실패했습니다 ({0}): {1}": "AI 引擎失败 ({0}): {1}",
|
||||
"AI 엔진이 없습니다. 환경 설정 > AI 그림에서 받아 주세요.": "没有 AI 引擎。请在 设置 > AI 图片 中下载。",
|
||||
"AI 엔진이 해상도를 올리거나 그리지 못했습니다 (처리 장치를 바꿔 보세요)": "AI 引擎未能放大或生成 (请尝试更换处理设备)",
|
||||
|
|
@ -820,6 +822,7 @@
|
|||
"멈춤 (이번 실행 동안)": "暂停 (本次运行期间)",
|
||||
"멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다": "已暂停 (本次运行期间) · 按开始按钮重新同步",
|
||||
"멈춤 · {0}": "已停止 · {0}",
|
||||
"메모리가 모자라 Android 가 AI 엔진을 끝냈습니다. 다른 앱을 닫거나 환경 설정의 해상도 올리기 메모리를 \"적게\" 로 해 보세요": "内存不足,Android 结束了 AI 引擎。请关闭其他应用,或在设置中将提高分辨率的内存设为“少”",
|
||||
"모니터링": "监控",
|
||||
"모니터링의 [실행]": "监视中的 [执行]",
|
||||
"모델 (선택)": "模型 (可选)",
|
||||
|
|
@ -1173,6 +1176,7 @@
|
|||
"실행 · 종료": "运行 · 退出",
|
||||
"실행 취소": "撤销",
|
||||
"실행한 rsync 가 없습니다. Rsync 화면에서 → · ← · ⇄ 로 실행하면 여기에 자동으로 등록됩니다.": "还没有运行过 rsync。在 Rsync 画面用 → · ← · ⇄ 运行后会自动登记到这里。",
|
||||
"실행해 보지 못함 ({0})": "无法运行({0})",
|
||||
"싱크": "同步",
|
||||
"쓰레드 확인 (AI · 변환 작업이 멈출 때까지)": "线程检查(直到 AI · 转换任务停止)",
|
||||
"쓸 수 없는 이름입니다": "不能使用此名称",
|
||||
|
|
@ -1413,8 +1417,10 @@
|
|||
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "本设备每张约 {0} (按上次耗时)",
|
||||
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "本设备每张约 {0} · 共约 {1} (按上次耗时)",
|
||||
"이 기기에서는 AI 번역을 쓸 수 없습니다.": "此设备无法使用 AI 翻译。",
|
||||
"이 기기에서는 AI 엔진을 찾지 못했습니다. 앱을 다시 설치해 보세요": "在此设备上找不到 AI 引擎。请尝试重新安装应用",
|
||||
"이 기기에서는 쓸 수 없는 컴포넌트입니다": "此组件在本设备上不可用",
|
||||
"이 기기용 파일 없음": "没有此设备的文件",
|
||||
"이 기기의 CPU 가 이 AI 엔진을 지원하지 않습니다": "此设备的 CPU 不支持该 AI 引擎",
|
||||
"이 기기의 안전 저장소를 쓸 수 없어 비밀번호를 저장하지 못했습니다": "本设备的安全存储不可用,无法保存密码",
|
||||
"이 기기의 폴더": "本设备上的文件夹",
|
||||
"이 단축키를 등록할 수 없습니다 (다른 프로그램이 사용 중일 수 있음)": "无法注册此快捷键(可能已被其他程序使用)",
|
||||
|
|
@ -1861,6 +1867,7 @@
|
|||
"터키어": "土耳其语",
|
||||
"토": "六",
|
||||
"토렌트 · 마그넷 다운로드": "种子 · 磁力链接下载",
|
||||
"통과": "通过",
|
||||
"트랙 #{0} ({1}){2}": "轨道 #{0}({1}){2}",
|
||||
"트랙 {0}": "轨道 {0}",
|
||||
"트레이 아이콘을 누르거나 아래 단축키로 다시 엽니다": "点击托盘图标或用下方快捷键重新打开",
|
||||
|
|
|
|||
|
|
@ -75,11 +75,17 @@ class AiStore extends ChangeNotifier {
|
|||
}
|
||||
|
||||
/// 받기 (SHA-256 확인). 저장 공간이 모자라면 먼저 알린다. zip (실행 파일) 은 푼다.
|
||||
Future<void> install(String id) => installFile(aiFile(id));
|
||||
/// 195: 받기 전 엔진 검사를 거치게 [installAll] 로 (하나만 받을 때도)
|
||||
Future<void> install(String id) => installAll([aiFile(id)]);
|
||||
|
||||
/// 여러 개를 차례로 (기다리는 것은 [queued] 로 보인다). 하나가 실패하면 멈추고 알린다
|
||||
Future<void> installAll(List<AiFile> files) async {
|
||||
final todo = [for (final f in files) if (!isFileInstalled(f) && !progress.containsKey(f.id)) f];
|
||||
// 195: 큰 모델을 받기 전에 이 기기에서 엔진이 도는지 (안 되면 받지 않고 이유를 알린다)
|
||||
if (todo.any((f) => f.kind != 'engine')) {
|
||||
final problem = await sdEngineProblem();
|
||||
if (problem != null) throw ImageAiException(problem);
|
||||
}
|
||||
queued.addAll(todo.map((f) => f.id));
|
||||
notifyListeners();
|
||||
try {
|
||||
|
|
@ -297,7 +303,8 @@ class AiStore extends ChangeNotifier {
|
|||
if (cuda) return null;
|
||||
try {
|
||||
final dir = await const MethodChannel('jj_mkvmaker/android').invokeMethod<String>('nativeLibDir');
|
||||
final f = dir == null ? null : File(p.join(dir, 'libsdcli.so'));
|
||||
// 195: dotprod · fp16 이 없는 CPU (ARMv8.0) 는 그 명령을 쓰지 않는 판으로 (v8.2 판은 SIGILL 로 죽는다)
|
||||
final f = dir == null ? null : File(p.join(dir, sdCliLibName(await _cpuInfo())));
|
||||
return f != null && f.existsSync() ? f.path : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
|
|
@ -309,6 +316,45 @@ class AiStore extends ChangeNotifier {
|
|||
return exe;
|
||||
}
|
||||
|
||||
static Future<String> _cpuInfo() async {
|
||||
try {
|
||||
return await File('/proc/cpuinfo').readAsString();
|
||||
} catch (_) {
|
||||
return ''; // 모르면 어느 CPU 에서나 도는 판 ([sdCliLibName])
|
||||
}
|
||||
}
|
||||
|
||||
/// 195: 큰 모델을 받기 전에 엔진을 짧게 실행해 본다 (도움말). 이 기기에서 못 돌면 쉬운 말 한 줄, 되면 null. 한 번만 본다
|
||||
Future<String?> sdEngineProblem() => _engineProblem ??= _checkEngine();
|
||||
Future<String?>? _engineProblem;
|
||||
|
||||
Future<String?> _checkEngine() async {
|
||||
if (!Platform.isAndroid) return null;
|
||||
final lib = sdCliLibName(await _cpuInfo());
|
||||
final exe = await sdCli();
|
||||
String? problem;
|
||||
String result;
|
||||
if (exe == null) {
|
||||
problem = tr('이 기기에서는 AI 엔진을 찾지 못했습니다. 앱을 다시 설치해 보세요');
|
||||
result = problem;
|
||||
} else {
|
||||
try {
|
||||
final r = await Process.run(exe, ['--help'], workingDirectory: p.dirname(exe)).timeout(const Duration(seconds: 20));
|
||||
// 신호로 죽었을 때만 (도움말은 0 이 아닌 코드로 끝날 수 있다)
|
||||
problem = r.exitCode < 0 ? sdExitMessage(r.exitCode) : null;
|
||||
result = problem ?? tr('통과');
|
||||
} catch (e) {
|
||||
result = trf('실행해 보지 못함 ({0})', [e.runtimeType]); // 받기는 막지 않는다
|
||||
}
|
||||
}
|
||||
// 관리자 지시: 기기별 문제를 볼 때 쓰게 작업 기록에 한 줄 (이 기기에서 고른 판 · 결과)
|
||||
log?.call(trf('AI 엔진 확인: {0} · {1}', [lib, result]));
|
||||
return problem;
|
||||
}
|
||||
|
||||
/// 작업 기록에 한 줄 (앱이 켜질 때 AppController.note 로 이어 둔다)
|
||||
static void Function(String line)? log;
|
||||
|
||||
static String? _find(Directory dir, String name) {
|
||||
if (!dir.existsSync()) return null;
|
||||
for (final e in dir.listSync(recursive: true, followLinks: false)) {
|
||||
|
|
@ -507,12 +553,27 @@ Future<List<String>> runSdCli(String exe, List<String> args,
|
|||
if (code != 0 && !saved) {
|
||||
final err = tail.where((l) => l.contains('[E]') || l.toLowerCase().contains('error')).toList();
|
||||
// 150: 쉬운 말 한 줄, 엔진 원문은 [자세히] 에
|
||||
throw ImageAiException(trf('AI 엔진이 멈췄습니다 (코드 {0}). 메모리가 모자라거나 파일이 깨졌을 수 있습니다', [code]),
|
||||
detail: (err.isEmpty ? tail : err).join('\n'));
|
||||
throw ImageAiException(sdExitMessage(code), detail: (err.isEmpty ? tail : err).join('\n'));
|
||||
}
|
||||
return tail;
|
||||
}
|
||||
|
||||
/// 195: Android sd-cli 판 고르기 - 모든 코어의 Features 에 asimddp (dotprod) · fphp (fp16) 가 있으면 빠른 v8.2 판,
|
||||
/// 아니면 (모름 포함) 어느 arm64 에서나 도는 v8.0 판 (tool/build_sdcpp_android.sh)
|
||||
String sdCliLibName(String cpuinfo) {
|
||||
final lines = [for (final l in cpuinfo.split('\n')) if (l.trimLeft().toLowerCase().startsWith('features')) l];
|
||||
bool has(String line, String f) => line.split(RegExp(r'[\s:]+')).contains(f);
|
||||
final v82 = lines.isNotEmpty && lines.every((l) => has(l, 'asimddp') && has(l, 'fphp'));
|
||||
return v82 ? 'libsdcli.so' : 'libsdcli_v80.so';
|
||||
}
|
||||
|
||||
/// 195: 엔진이 끝난 코드 → 쉬운 말 (음수 = 그 신호로 죽음). 메모리 · 파일 탓은 실제 그 원인일 때만 말한다
|
||||
String sdExitMessage(int code) => switch (code) {
|
||||
-4 => tr('이 기기의 CPU 가 이 AI 엔진을 지원하지 않습니다'), // SIGILL: CPU 에 없는 명령
|
||||
-9 => tr('메모리가 모자라 Android 가 AI 엔진을 끝냈습니다. 다른 앱을 닫거나 환경 설정의 해상도 올리기 메모리를 "적게" 로 해 보세요'),
|
||||
_ => trf('AI 엔진이 멈췄습니다 (코드 {0}). 엔진 기록은 [자세히] 에 있습니다', [code]),
|
||||
};
|
||||
|
||||
/// 처리 장치 후보 (Windows: CUDA 판 · Vulkan 판의 GPU · CPU, Android: CPU)
|
||||
Future<List<SdDevice>> sdDevices(AiStore store) async {
|
||||
final out = <SdDevice>[];
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ Future<void> main(List<String> args) async {
|
|||
final freshInstall = !File(p.join(dataDir, 'settings.json')).existsSync();
|
||||
ComponentStore.dataDirectory = dataDir;
|
||||
AiStore.instance = AiStore(dataDir); // 123 · 121: AI 모델 · 엔진 (앱 데이터 폴더의 ai)
|
||||
AiStore.log = controller.note;
|
||||
VersionSnapshot.instance = VersionSnapshot(dataDir);
|
||||
await controller.init();
|
||||
await setupMasterLock(controller); // 124: 저장된 비밀번호를 쓰기 전에 (실시간 동기화보다 먼저)
|
||||
|
|
@ -477,6 +478,7 @@ Future<void> runAndroid(String dataDir) async {
|
|||
if (autoBackup) controller.note(tr('Android 자동 백업에서 되살린 설정으로 시작합니다'));
|
||||
ComponentStore.dataDirectory = dataDir;
|
||||
AiStore.instance = AiStore(dataDir); // 123 · 121: AI 모델 · 엔진 (앱 데이터 폴더의 ai)
|
||||
AiStore.log = controller.note;
|
||||
String? sharedSnapshots;
|
||||
try {
|
||||
final root = await const MethodChannel('jj_mkvmaker/android').invokeMethod<String>('storageRoot');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/ai_local.dart';
|
||||
|
||||
/// 195: Android 의 AI 엔진 (sd-cli) 판 고르기 · 엔진이 죽은 신호별 문구
|
||||
void main() {
|
||||
// 실제 기기의 /proc/cpuinfo Features 줄 (a4 확인)
|
||||
const p11 = 'processor\t: 0\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid\n'
|
||||
'processor\t: 7\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid\n'; // Snapdragon 662 (ARMv8.0)
|
||||
const note10 = 'processor\t: 0\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\n'
|
||||
'processor\t: 4\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp\n';
|
||||
const s10 = 'Features\t: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 '
|
||||
'sm3 sm4 asimddp sha512 asimdfhm dit uscat ilrcpc flagm ssbs sb paca pacg dcpodp flagm2 frint i8mm bf16 bti\n';
|
||||
|
||||
test('195: dotprod · fp16 이 없는 CPU (P11) 는 v8.0 판, 있으면 (노트 10+ · S10) 빠른 판', () {
|
||||
expect(sdCliLibName(p11), 'libsdcli_v80.so');
|
||||
expect(sdCliLibName(note10), 'libsdcli.so');
|
||||
expect(sdCliLibName(s10), 'libsdcli.so');
|
||||
});
|
||||
|
||||
test('195: 모르면 (읽지 못함 · 코어마다 다름) 어느 CPU 에서나 도는 판', () {
|
||||
expect(sdCliLibName(''), 'libsdcli_v80.so');
|
||||
// 한 코어라도 없으면 (그 코어에서 돌면 죽으므로)
|
||||
expect(sdCliLibName('Features\t: fp asimd asimddp fphp\nFeatures\t: fp asimd\n'), 'libsdcli_v80.so');
|
||||
// 이름이 비슷한 다른 기능 (asimdhp 만) 은 fphp 로 치지 않는다
|
||||
expect(sdCliLibName('Features\t: fp asimd asimddp asimdhp\n'), 'libsdcli_v80.so');
|
||||
});
|
||||
|
||||
test('195: 엔진이 죽은 신호별 문구 - CPU 미지원 · 메모리는 그 신호일 때만', () {
|
||||
expect(sdExitMessage(-4), contains('CPU'));
|
||||
expect(sdExitMessage(-4), isNot(contains('메모리')));
|
||||
expect(sdExitMessage(-9), contains('메모리'));
|
||||
for (final code in [1, -6, -11]) {
|
||||
expect(sdExitMessage(code), contains('$code'));
|
||||
expect(sdExitMessage(code), isNot(contains('메모리')));
|
||||
expect(sdExitMessage(code), isNot(contains('깨졌')));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -3,9 +3,18 @@
|
|||
# third_party/sdcpp/android/arm64-v8a/libsdcli.so 로 둔다 (APK 의 jniLibs 로 들어가 앱의 네이티브 라이브러리 폴더에서 실행된다 -
|
||||
# Android 10+ 는 앱 데이터 폴더의 파일을 실행할 수 없으므로 받아서 쓰지 않는다).
|
||||
# 소스: https://github.com/leejet/stable-diffusion.cpp (MIT) 커밋 고정 - Windows 실행 파일 (ai_catalog.dart 의 sdCppVersion) 과 같은 판
|
||||
# CPU (arm64, dotprod · i8mm · fp16) 만. OpenMP 는 끈다.
|
||||
# 사용법 (Git Bash): sh tool/build_sdcpp_android.sh - Android NDK · SDK 의 cmake (tools/android-sdk) 가 필요
|
||||
# CPU 만 (OpenMP 는 끈다). 두 벌 (195):
|
||||
# v82 (기본) = armv8.2-a + dotprod · i8mm · fp16 → libsdcli.so (빠름, S10 · 노트 10+ 등)
|
||||
# v80 = armv8-a (dotprod · fp16 없는 보급형 CPU, 예: Snapdragon 662) → libsdcli_v80.so
|
||||
# 앱은 /proc/cpuinfo 의 asimddp · fphp 를 보고 고른다 (lib/app/ai_local.dart sdCli). v82 를 v8.0 CPU 에서 돌리면 SIGILL 로 죽는다.
|
||||
# 사용법 (Git Bash): sh tool/build_sdcpp_android.sh [v82|v80] - Android NDK · SDK 의 cmake (tools/android-sdk) 가 필요
|
||||
set -e
|
||||
VARIANT=${1:-v82}
|
||||
case "$VARIANT" in
|
||||
v82) ARCH=armv8.2-a+dotprod+i8mm+fp16; NAME=libsdcli.so ;;
|
||||
v80) ARCH=armv8-a; NAME=libsdcli_v80.so ;;
|
||||
*) echo "모르는 판: $VARIANT (v82 · v80)"; exit 1 ;;
|
||||
esac
|
||||
APP=$(cd "$(dirname "$0")/.." && pwd)
|
||||
COMMIT=228c707fde018221de74674f1c2f480a9d2b228e
|
||||
SHORT=228c707
|
||||
|
|
@ -23,12 +32,12 @@ git fetch --depth 1 origin "$COMMIT" 2>/dev/null || git fetch origin
|
|||
git checkout -q "$COMMIT" 2>/dev/null || git checkout -q "$SHORT"
|
||||
git submodule update --init --recursive
|
||||
PATH="$CMAKE_DIR:$PATH"
|
||||
cmake -S . -B build-android -G Ninja -DCMAKE_MAKE_PROGRAM="$CMAKE_DIR/ninja$EXE" \
|
||||
cmake -S . -B build-android-$VARIANT -G Ninja -DCMAKE_MAKE_PROGRAM="$CMAKE_DIR/ninja$EXE" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$NDK_ROOT/build/cmake/android.toolchain.cmake" \
|
||||
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-28 -DCMAKE_BUILD_TYPE=Release \
|
||||
-DGGML_OPENMP=OFF -DGGML_CPU_ARM_ARCH=armv8.2-a+dotprod+i8mm+fp16 > cmake.log
|
||||
cmake --build build-android --target sd-cli -j 8 > build.log 2>&1
|
||||
-DGGML_OPENMP=OFF -DGGML_CPU_ARM_ARCH=$ARCH > cmake-$VARIANT.log
|
||||
cmake --build build-android-$VARIANT --target sd-cli -j 8 > build-$VARIANT.log 2>&1
|
||||
OUT="$APP/third_party/sdcpp/android/arm64-v8a"
|
||||
mkdir -p "$OUT"
|
||||
"$TC/llvm-strip$EXE" -o "$OUT/libsdcli.so" build-android/bin/sd-cli
|
||||
ls -l "$OUT/libsdcli.so"
|
||||
"$TC/llvm-strip$EXE" -o "$OUT/$NAME" build-android-$VARIANT/bin/sd-cli
|
||||
ls -l "$OUT/$NAME"
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
# - 이 파일이 빠지면 플러그인이 하나도 등록되지 않아 켜자마자 꺼진다 ('JNI is not initialized … too early', 묶음5 a192ef6 시험판).
|
||||
# - 원인: android\app\src\main\java\io\flutter\plugins\GeneratedPluginRegistrant.java (git 밖, 생성 파일) 를 지운 뒤
|
||||
# flutter build apk --no-pub 로 빌드하면 다시 만들어지지 않는다. 지웠으면 --no-pub 없이 빌드하거나 먼저 flutter pub get.
|
||||
# - (195) third_party 의 네이티브 실행 파일: android\app\third_party_libs.txt 의 것이 모두 들어 있고, 목록에 없는 third_party 파일은 없는지.
|
||||
# 사용: powershell -ExecutionPolicy Bypass -File tool\check_apk.ps1 -Apk build\app\outputs\flutter-apk\app-release.apk
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Apk
|
||||
[Parameter(Mandatory = $true)][string]$Apk,
|
||||
# 목록 파일 (기본: 이 스크립트가 있는 앱 폴더의 android\app\third_party_libs.txt)
|
||||
[string]$Libs = ''
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
|
|
@ -26,8 +29,18 @@ function Test-Bytes([byte[]]$hay, [byte[]]$needle) {
|
|||
return $false
|
||||
}
|
||||
|
||||
$app = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||
if (-not $Libs) { $Libs = Join-Path $app 'android\app\third_party_libs.txt' }
|
||||
$listed = @(Get-Content $Libs | ForEach-Object { $_.Trim() } | Where-Object { $_ -and -not $_.StartsWith('#') })
|
||||
# third_party 에 있는 파일 (목록 밖이면 APK 에 없어야 함)
|
||||
$tp = @()
|
||||
foreach ($d in 'third_party\rsync\android', 'third_party\sdcpp\android') {
|
||||
$dir = Join-Path $app $d
|
||||
if (Test-Path $dir) { $tp += @(Get-ChildItem $dir -Recurse -Filter *.so | ForEach-Object { $_.Name }) }
|
||||
}
|
||||
$zip = [System.IO.Compression.ZipFile]::OpenRead($Apk)
|
||||
try {
|
||||
$libsIn = @($zip.Entries | Where-Object { $_.FullName -like 'lib/arm64-v8a/*.so' } | ForEach-Object { $_.Name })
|
||||
$dexes = @($zip.Entries | Where-Object { $_.FullName -match '^classes\d*\.dex$' })
|
||||
if ($dexes.Count -eq 0) { throw "dex 가 없습니다: $Apk" }
|
||||
$found = $null
|
||||
|
|
@ -40,8 +53,15 @@ try {
|
|||
} finally {
|
||||
$zip.Dispose()
|
||||
}
|
||||
$bad = @()
|
||||
foreach ($n in $listed) { if ($libsIn -notcontains $n) { $bad += "목록에 있는데 APK 에 없음: $n" } }
|
||||
foreach ($n in ($tp | Select-Object -Unique)) { if ($listed -notcontains $n -and $libsIn -contains $n) { $bad += "목록에 없는데 APK 에 들어감: $n" } }
|
||||
if ($bad) {
|
||||
$bad | ForEach-Object { Write-Host "실패: $_ ($Libs)" -ForegroundColor Red }
|
||||
exit 1
|
||||
}
|
||||
if (-not $found) {
|
||||
Write-Host "실패: GeneratedPluginRegistrant 가 dex 에 없습니다 (켜자마자 꺼지는 APK). flutter pub get 뒤 --no-pub 없이 다시 빌드하세요." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "통과: GeneratedPluginRegistrant 있음 ($found)"
|
||||
Write-Host "통과: GeneratedPluginRegistrant 있음 ($found) · third_party 실행 파일 $($listed.Count)개 목록과 같음"
|
||||
|
|
|
|||
Loading…
Reference in New Issue