import React, { useEffect, useMemo, useState } from 'react'; import { StatusBar } from 'expo-status-bar'; import { useKeepAwake } from 'expo-keep-awake'; import { Asset } from 'expo-asset'; import { ActivityIndicator, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native'; import { NativeModules } from 'react-native'; import * as DocumentPicker from 'expo-document-picker'; import * as FileSystem from 'expo-file-system/legacy'; import { createClipTokenizer } from './clipTokenizer'; const BUILD_ID = '2026-02-12-v2-speed-exp-002'; const QUICK_STEPS = 4; const QUALITY_STEPS = 20; const GUIDE_STEPS = [ { key: 'tapLaunch', label: 'Tap to confirm launch' }, { key: 'showExternalPath', label: 'Show app external pack path' }, { key: 'unzipExternal', label: 'Unzip SD15 pack from external' }, { key: 'loadTokenizer', label: 'Load tokenizer (bundled)' }, { key: 'testNative', label: 'Test native ONNX (NNAPI -> CPU)' }, { key: 'inspectUnet', label: 'Inspect pack UNet' }, { key: 'inspectText', label: 'Inspect pack text_encoder' }, { key: 'inspectVae', label: 'Inspect pack vae_decoder' }, { key: 'generateQuick', label: `Generate 512x512 (quick ${QUICK_STEPS})` }, { key: 'generateQuality', label: `Generate 512x512 (quality ${QUALITY_STEPS})` }, { key: 'backendToggle', label: 'Backend: NNAPI / CPU' }, { key: 'importOnnx', label: 'Import ONNX files (offline)' }, { key: 'inspectImported', label: 'Inspect first imported model' }, { key: 'importPackZip', label: 'Import SD15 pack (.zip)' }, ] as const; type GuideStepKey = (typeof GUIDE_STEPS)[number]['key']; export default function App() { useKeepAwake(); const [taps, setTaps] = useState(0); const [busy, setBusy] = useState(false); const [provider, setProvider] = useState('-'); const [output, setOutput] = useState('-'); const [err, setErr] = useState(''); const [status, setStatus] = useState('Idle'); const [lastExportPath, setLastExportPath] = useState('-'); const [importedFiles, setImportedFiles] = useState([]); const [packDir, setPackDir] = useState(''); const [packFiles, setPackFiles] = useState(0); const [externalDir, setExternalDir] = useState(''); const [prompt, setPrompt] = useState('A photo of a lion in the wild, ultra realistic'); const [tokenizerReady, setTokenizerReady] = useState(false); const [preferNnapi, setPreferNnapi] = useState(true); const [guidedMode, setGuidedMode] = useState(true); const [guideIndex, setGuideIndex] = useState(0); const canRun = useMemo(() => true, []); const guideDone = guideIndex >= GUIDE_STEPS.length; const currentGuideStep = guideDone ? null : GUIDE_STEPS[guideIndex]; const showGuideButton = (key: GuideStepKey) => !guidedMode || guideDone || currentGuideStep?.key === key; const completeGuideStep = (key: GuideStepKey) => { if (!guidedMode || guideDone) return; if (currentGuideStep?.key !== key) return; setGuideIndex((n) => n + 1); }; useEffect(() => { let cancelled = false; (async () => { try { const mod = (NativeModules as any)?.OrtNative; if (!mod?.getExternalFilesDirPath) return; const p = await mod.getExternalFilesDirPath(); if (cancelled) return; const ext = String(p || ''); if (!ext) return; setExternalDir((prev) => prev || ext); if (packDir) return; if (!mod?.statPath) return; const candidate = `${ext.replace(/\/+$/, '')}/sd15-onnx/sd15-onnx`; const st = await mod.statPath(`file://${candidate}/unet.onnx`); if (cancelled) return; if (st?.exists) setPackDir((prev) => prev || candidate); } catch { // Non-fatal; user can still set paths via buttons. } })(); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const packFileUri = (filename: string) => { const base = packDir.startsWith('file://') ? packDir.slice('file://'.length) : packDir; const baseNoSlash = base.replace(/\/+$/, ''); return `file://${baseNoSlash}/${filename}`; }; const tokenizer = useMemo(() => { if (!tokenizerReady) return null; return (globalThis as any).__sp_clip_tokenizer__ ?? null; }, [tokenizerReady]); const yieldToUi = async () => new Promise((r) => setTimeout(r, 0)); const runGenerate = async (steps: number, forceCpu: boolean = false, lowMemoryMode: boolean = true) => { setBusy(true); setErr(''); setProvider('-'); setOutput('-'); try { if (!packDir) throw new Error('Pack not set. Tap "Unzip SD15 pack from external" (once) if needed.'); if (!externalDir) throw new Error('External dir is not set. Tap "Show app external pack path" first.'); if (!tokenizer) throw new Error('Tokenizer not loaded. Tap "Load tokenizer (bundled)" first.'); const mod = (NativeModules as any)?.OrtNative; if (!mod?.generateSd15FromIds) throw new Error('Native module OrtNative.generateSd15FromIds is not available (rebuild required).'); setStatus('Tokenizing...'); await yieldToUi(); const condIds = tokenizer.encode(prompt, 77); const uncondIds = tokenizer.encode('', 77); const outPath = `file://${externalDir.replace(/\/+$/, '')}/sd_out_${Date.now()}.png`; setStatus(`Generating... (${steps} steps)`); await yieldToUi(); const useNnapi = forceCpu ? false : preferNnapi; const runOnce = async (nnapi: boolean, lowMem: boolean) => mod.generateSd15FromIds({ packDir, condIds, uncondIds, steps, guidance: 7.5, seed: -1, outPath, preferNnapi: nnapi, lowMemoryMode: lowMem, }); let res: any; try { res = await runOnce(useNnapi, lowMemoryMode); } catch (firstErr: any) { // In v2, if NNAPI path fails without killing process, retry CPU automatically. if (useNnapi) { setStatus('NNAPI failed, retrying CPU...'); await yieldToUi(); res = await runOnce(false, true); } else { throw firstErr; } } try { if (mod?.exportImageToGallery && res?.path) { const fileName = String(res.path).split('/').pop() || `sd_out_${Date.now()}.png`; const exported = await mod.exportImageToGallery(String(res.path), fileName); setLastExportPath(String(exported || `Pictures/Staticplay/${fileName}`)); } } catch (exportErr: any) { // Non-fatal: image remains in app external storage even if gallery export fails. const msg = String(exportErr?.message ?? exportErr ?? '').trim(); if (msg) { setErr((prev) => (prev ? `${prev}\nExport: ${msg}` : `Export: ${msg}`)); } } setProvider(String(res?.provider ?? 'unknown')); setOutput(JSON.stringify(res)); setStatus('Done'); } catch (e: any) { const msg = String(e?.message ?? e ?? '').trim(); setErr(msg || 'Generation failed (unknown error)'); setStatus('Error'); } finally { setBusy(false); } }; return ( Staticplay Offline-first SD1 on-device (Android) - {BUILD_ID} Guided mode {guidedMode ? guideDone ? 'Done. All guided steps completed.' : `Step ${guideIndex + 1}/${GUIDE_STEPS.length}: ${currentGuideStep?.label}` : 'Off. Showing all buttons.'} setGuidedMode((v) => !v)}> {guidedMode ? 'Show all buttons' : 'Enable guided mode'} { setGuideIndex(0); setGuidedMode(true); }} > Restart guide Prompt 512x512 - quick {QUICK_STEPS} / quality {QUALITY_STEPS} - guidance 7.5 {showGuideButton('tapLaunch') ? ( { setTaps((n) => n + 1); completeGuideStep('tapLaunch'); }} > Tap to confirm launch ({taps}) ) : null} {showGuideButton('backendToggle') ? ( { setPreferNnapi((v) => !v); completeGuideStep('backendToggle'); }} > {preferNnapi ? 'Backend: NNAPI (experimental)' : 'Backend: CPU (safe)'} ) : null} {showGuideButton('loadTokenizer') ? ( { setBusy(true); setErr(''); let loaded = false; try { setStatus('Loading tokenizer...'); const vocabAsset = Asset.fromModule(require('./assets/tokenizer/vocab.json.txt')); const mergesAsset = Asset.fromModule(require('./assets/tokenizer/merges.txt')); await vocabAsset.downloadAsync(); await mergesAsset.downloadAsync(); const vocabUri = vocabAsset.localUri || vocabAsset.uri; const mergesUri = mergesAsset.localUri || mergesAsset.uri; if (!vocabUri) throw new Error('Tokenizer vocab asset missing local URI.'); if (!mergesUri) throw new Error('Tokenizer merges asset missing local URI.'); const mod = (NativeModules as any)?.OrtNative; const readText = mod?.readTextFile ? async (uri: string) => String(await mod.readTextFile(uri)) : async (uri: string) => String(await FileSystem.readAsStringAsync(uri)); let vocabJson = ''; let mergesTxt = ''; try { vocabJson = await readText(vocabUri); mergesTxt = await readText(mergesUri); } catch { // Fallback: read tokenizer from app external storage if bundled read fails. if (!externalDir) throw new Error('Tokenizer read failed and external dir is not set.'); const extBase = externalDir.replace(/\/+$/, ''); vocabJson = await readText(`file://${extBase}/tokenizer/vocab.json.txt`); mergesTxt = await readText(`file://${extBase}/tokenizer/merges.txt`); } (globalThis as any).__sp_clip_tokenizer__ = createClipTokenizer({ vocabJson, mergesTxt }); setTokenizerReady(true); setStatus('OK'); loaded = true; } catch (e: any) { const msg = String(e?.message ?? e ?? '').trim(); setErr(msg || 'Tokenizer load failed (unknown error)'); setStatus('Error'); } finally { setBusy(false); if (loaded) completeGuideStep('loadTokenizer'); } }} > {busy ? 'Working...' : tokenizerReady ? 'Tokenizer loaded' : 'Load tokenizer (bundled)'} ) : null} {showGuideButton('generateQuick') ? ( { // v2 quick path: NNAPI-first and less memory-constrained for speed. await runGenerate(QUICK_STEPS, false, false); completeGuideStep('generateQuick'); }} > {busy ? 'Working...' : `Generate 512x512 (quick ${QUICK_STEPS})`} ) : null} {showGuideButton('generateQuality') ? ( { // v2 quality path: keep stable settings. await runGenerate(QUALITY_STEPS, true, true); completeGuideStep('generateQuality'); }} > {busy ? 'Working...' : `Generate 512x512 (quality ${QUALITY_STEPS})`} ) : null} {showGuideButton('testNative') ? ( { setBusy(true); setErr(''); setProvider('-'); setOutput('-'); setStatus('Running identity ONNX...'); try { const mod = (NativeModules as any)?.OrtNative; if (!mod?.runIdentity) throw new Error('Native module OrtNative is not available (rebuild required).'); const res = await mod.runIdentity([1, 2, 3]); setProvider(String(res?.provider ?? 'unknown')); const outArr = res?.output ?? []; setOutput(JSON.stringify(outArr)); setStatus('OK'); } catch (e: any) { setErr(e?.message || String(e)); setStatus('Error'); } finally { setBusy(false); completeGuideStep('testNative'); } }} > {busy ? 'Running...' : 'Test native ONNX (NNAPI -> CPU)'} ) : null} {showGuideButton('importOnnx') ? ( { setBusy(true); setErr(''); setStatus('Picking ONNX files...'); try { const res = await DocumentPicker.getDocumentAsync({ multiple: true, type: ['application/octet-stream', '*/*'], copyToCacheDirectory: true, }); if (res.canceled) return; const destDir = `${FileSystem.documentDirectory}models/sd1/`; await FileSystem.makeDirectoryAsync(destDir, { intermediates: true }); const saved: string[] = []; for (const asset of res.assets) { const name = (asset.name || 'model.onnx').replace(/[^\w.\-]+/g, '_'); const to = `${destDir}${name}`; await FileSystem.copyAsync({ from: asset.uri, to }); saved.push(to); } setImportedFiles((prev) => Array.from(new Set([...saved, ...prev]))); setStatus(`Imported ${saved.length} file(s)`); } catch (e: any) { setErr(e?.message || String(e)); setStatus('Error'); } finally { setBusy(false); completeGuideStep('importOnnx'); } }} > {busy ? 'Working...' : 'Import ONNX files (offline)'} ) : null} {showGuideButton('inspectImported') ? ( { setBusy(true); setErr(''); setProvider('-'); setOutput('-'); setStatus('Inspecting...'); try { if (importedFiles.length === 0) throw new Error('No imported ONNX files yet. Tap "Import ONNX files (offline)" first.'); const mod = (NativeModules as any)?.OrtNative; if (!mod?.inspectModel) throw new Error('Native module OrtNative.inspectModel is not available (rebuild required).'); const target = importedFiles[0]; const info = await mod.inspectModel(target); setProvider(String(info?.provider ?? 'unknown')); setOutput(JSON.stringify({ file: target.split('/').pop(), inputs: info?.inputs, outputs: info?.outputs })); setStatus('OK'); } catch (e: any) { setErr(e?.message || String(e)); setStatus('Error'); } finally { setBusy(false); completeGuideStep('inspectImported'); } }} > Inspect first imported model ) : null} {showGuideButton('importPackZip') ? ( { setBusy(true); setErr(''); setStatus('Picking zip...'); try { const res = await DocumentPicker.getDocumentAsync({ multiple: false, type: ['application/zip', 'application/octet-stream', '*/*'], copyToCacheDirectory: false, }); if (res.canceled) return; const asset = res.assets[0]; const zipUri = asset.uri; const destDir = `${FileSystem.documentDirectory}packs/sd15-onnx/`; await FileSystem.makeDirectoryAsync(destDir, { intermediates: true }); const mod = (NativeModules as any)?.OrtNative; if (!mod?.unzip) throw new Error('Native module OrtNative.unzip is not available (rebuild required).'); if (mod?.statPath) { const st = await mod.statPath(zipUri); if (st && st.exists === false) throw new Error(`Zip not found: ${String(st.path || zipUri)}`); } setStatus('Unzipping... (this can take minutes)'); const out = await mod.unzip(zipUri, destDir); { const base = String(out?.destPath || destDir); const normalized = base.startsWith('file://') ? base.slice('file://'.length) : base; setPackDir(`${normalized.replace(/\/+$/, '')}/sd15-onnx`); } setPackFiles(Number(out?.files || 0)); setStatus(`Unzipped ${Number(out?.files || 0)} file(s)`); } catch (e: any) { setErr(e?.message || String(e)); setStatus('Error'); } finally { setBusy(false); completeGuideStep('importPackZip'); } }} > {busy ? 'Working...' : 'Import SD15 pack (.zip)'} ) : null} {showGuideButton('inspectUnet') ? ( { setBusy(true); setErr(''); setProvider('-'); setOutput('-'); setStatus('Inspecting UNet...'); try { if (!packDir) throw new Error('Pack not set. Tap "Import SD15 pack (.zip)" or "Unzip SD15 pack from external" first.'); const mod = (NativeModules as any)?.OrtNative; if (!mod?.inspectModel) throw new Error('Native module OrtNative.inspectModel is not available (rebuild required).'); const info = await mod.inspectModel(packFileUri('unet.onnx')); setProvider(String(info?.provider ?? 'unknown')); setOutput(JSON.stringify({ file: 'unet.onnx', inputs: info?.inputs, outputs: info?.outputs })); setStatus('OK'); } catch (e: any) { setErr(e?.message || String(e)); setStatus('Error'); } finally { setBusy(false); completeGuideStep('inspectUnet'); } }} > Inspect pack UNet ) : null} {showGuideButton('inspectText') ? ( { setBusy(true); setErr(''); setProvider('-'); setOutput('-'); setStatus('Inspecting text_encoder (CPU)...'); try { if (!packDir) throw new Error('Pack not set. Tap "Import SD15 pack (.zip)" or "Unzip SD15 pack from external" first.'); const mod = (NativeModules as any)?.OrtNative; if (!mod?.inspectModelCpu) throw new Error('Native module OrtNative.inspectModelCpu is not available (rebuild required).'); const info = await mod.inspectModelCpu(packFileUri('text_encoder.onnx')); setProvider(String(info?.provider ?? 'unknown')); setOutput(JSON.stringify({ file: 'text_encoder.onnx', inputs: info?.inputs, outputs: info?.outputs })); setStatus('OK'); } catch (e: any) { setErr(e?.message || String(e)); setStatus('Error'); } finally { setBusy(false); completeGuideStep('inspectText'); } }} > Inspect pack text_encoder ) : null} {showGuideButton('inspectVae') ? ( { setBusy(true); setErr(''); setProvider('-'); setOutput('-'); setStatus('Inspecting vae_decoder (CPU)...'); try { if (!packDir) throw new Error('Pack not set. Tap "Import SD15 pack (.zip)" or "Unzip SD15 pack from external" first.'); const mod = (NativeModules as any)?.OrtNative; if (!mod?.inspectModelCpu) throw new Error('Native module OrtNative.inspectModelCpu is not available (rebuild required).'); const info = await mod.inspectModelCpu(packFileUri('vae_decoder.onnx')); setProvider(String(info?.provider ?? 'unknown')); setOutput(JSON.stringify({ file: 'vae_decoder.onnx', inputs: info?.inputs, outputs: info?.outputs })); setStatus('OK'); } catch (e: any) { setErr(e?.message || String(e)); setStatus('Error'); } finally { setBusy(false); completeGuideStep('inspectVae'); } }} > Inspect pack vae_decoder ) : null} {showGuideButton('showExternalPath') ? ( { setBusy(true); setErr(''); try { setStatus('Getting external path...'); const mod = (NativeModules as any)?.OrtNative; if (!mod?.getExternalFilesDirPath) throw new Error('Native module OrtNative.getExternalFilesDirPath is not available (rebuild required).'); const p = await mod.getExternalFilesDirPath(); setExternalDir(String(p || '')); setStatus('OK'); } catch (e: any) { setErr(e?.message || String(e)); setStatus('Error'); } finally { setBusy(false); completeGuideStep('showExternalPath'); } }} > {busy ? 'Working...' : 'Show app external pack path'} ) : null} {showGuideButton('unzipExternal') ? ( { setBusy(true); setErr(''); try { if (!externalDir) throw new Error('External dir is not set. Tap "Show app external pack path" first.'); const mod = (NativeModules as any)?.OrtNative; if (!mod?.unzip) throw new Error('Native module OrtNative.unzip is not available (rebuild required).'); const extBase = externalDir.replace(/\/+$/, ''); const zipCandidates = [ `file://${extBase}/sd15-onnx-pack.zip`, 'file:///storage/emulated/0/Android/data/com.anonymous.staticplaysd1mobile/files/sd15-onnx-pack.zip', 'file:///storage/emulated/0/Download/sd15-onnx-pack.zip', ]; let zipUri = zipCandidates[0]; if (mod?.statPath) { let found = ''; for (const candidate of zipCandidates) { const st = await mod.statPath(candidate); if (st?.exists) { found = candidate; break; } } if (!found) throw new Error(`Zip not found in any expected location: ${zipCandidates.join(' | ')}`); zipUri = found; } const destDir = `file://${externalDir.replace(/\/+$/, '')}/sd15-onnx/`; setStatus('Unzipping... (this can take minutes)'); const out = await mod.unzip(zipUri, destDir); { const base = String(out?.destPath || destDir); const normalized = base.startsWith('file://') ? base.slice('file://'.length) : base; setPackDir(`${normalized.replace(/\/+$/, '')}/sd15-onnx`); } setPackFiles(Number(out?.files || 0)); setStatus(`Unzipped ${Number(out?.files || 0)} file(s)`); } catch (e: any) { setErr(e?.message || String(e)); setStatus('Error'); } finally { setBusy(false); completeGuideStep('unzipExternal'); } }} > {busy ? 'Working...' : 'Unzip SD15 pack from external'} ) : null} {busy ? : null} Status {status} Next See docs/SD1_ON_DEVICE_PLAN.md Native provider {provider} Native output {output} {err ? ( <> Error {err} ) : null} Last export path {lastExportPath || '-'} Imported files {importedFiles.length ? importedFiles.map((p) => p.split('/').pop()).join(', ') : '-'} Pack dir {packDir || '-'} Pack files {packDir ? String(packFiles) : '-'} External dir {externalDir || '-'} ); } const styles = StyleSheet.create({ safe: { flex: 1, backgroundColor: '#0b0f19' }, container: { padding: 18, gap: 12 }, title: { color: '#eaf0ff', fontSize: 22, fontWeight: '900' }, sub: { color: '#8aa0c4', fontSize: 12 }, btn: { backgroundColor: '#2563eb', paddingVertical: 12, paddingHorizontal: 14, borderRadius: 12, alignItems: 'center', marginTop: 6, }, btnDisabled: { opacity: 0.5 }, btnText: { color: 'white', fontWeight: '800' }, card: { backgroundColor: 'rgba(0,0,0,0.25)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.09)', borderRadius: 12, padding: 14, marginTop: 8, }, label: { color: '#9db2d6', fontSize: 12 }, value: { color: '#eaf0ff', marginTop: 4 }, input: { color: '#eaf0ff', marginTop: 8, padding: 10, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.05)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.10)', minHeight: 80, textAlignVertical: 'top', }, row: { flexDirection: 'row', gap: 8, marginTop: 10, }, smallBtn: { borderRadius: 10, paddingVertical: 8, paddingHorizontal: 10, }, smallBtnPrimary: { backgroundColor: '#1d4ed8', }, smallBtnGhost: { backgroundColor: '#1f2937', borderWidth: 1, borderColor: '#334155', }, smallBtnText: { color: '#eaf0ff', fontWeight: '700', fontSize: 12, }, });