Download manga_translator/server/ui.html from Sunday01/testapi: direct link, hf CLI and curl.
- Browser
- Download file 15.4 kB
-
https://huggingface.co/spaces/Sunday01/testapi/resolve/main/manga_translator/server/ui.html
- Command line
-
hf download hf://spaces/Sunday01/testapi/manga_translator/server/ui.html
-
curl -L -o ui.html https://huggingface.co/spaces/Sunday01/testapi/resolve/main/manga_translator/server/ui.html
15.4 kB
| <html> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <title>Image/Manga Translator</title> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@unocss/reset/tailwind.min.css" /> | |
| <script src="https://cdn.jsdelivr.net/npm/petite-vue@0.4.1/dist/petite-vue.iife.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@unocss/runtime@0.30.5/uno.global.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@iconify/iconify@2.2.0/dist/iconify.min.js"></script> | |
| <style> | |
| [v-cloak], | |
| [un-cloak] { | |
| display: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <form | |
| action="#" | |
| class="flex py-8 w-full min-h-100vh justify-center items-center" | |
| @submit.prevent="onsubmit" | |
| @vue:mounted="onmounted" | |
| v-scope | |
| v-cloak | |
| un-cloak | |
| > | |
| <div class="flex flex-col w-85ch h-full justify-center gap-2"> | |
| <h1 class="text-center text-lg font-light">Image/Manga Translator</h1> | |
| <div class="flex mx-4 justify-start items-end"> | |
| <div class="flex gap-4"> | |
| <div class="flex items-center" title="Detection resolution"> | |
| <i class="iconify" data-icon="carbon:fit-to-screen"></i> | |
| <div class="relative"> | |
| <select class="w-9ch appearance-none bg-transparent border-b border-gray-300" v-model="detectionResolution"> | |
| <option value="S">1024px</option> | |
| <option value="M">1536px</option> | |
| <option value="L">2048px</option> | |
| <option value="X">2560px</option> | |
| </select> | |
| <i class="iconify absolute top-1.5 right-1 pointer-events-none" data-icon="carbon:chevron-down"></i> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-1" title="Text detector"> | |
| <i class="iconify" data-icon="carbon:search-locate"></i> | |
| <div class="relative"> | |
| <select class="w-9ch appearance-none bg-transparent border-b border-gray-300" v-model="textDetector"> | |
| <option value="auto">Default</option> | |
| <option value="ctd">CTD</option> | |
| </select> | |
| <i class="iconify absolute top-1.5 right-1 pointer-events-none" data-icon="carbon:chevron-down"></i> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-1" title="Render text orientation"> | |
| <i class="iconify" data-icon="carbon:text-align-left"></i> | |
| <div class="relative"> | |
| <select class="w-12ch appearance-none bg-transparent border-b border-gray-300" v-model="renderTextDirection"> | |
| <option value="auto">Auto</option> | |
| <option value="h">Horizontal</option> | |
| <option value="v">Vertical</option> | |
| </select> | |
| <i class="iconify absolute top-1.5 right-1 pointer-events-none" data-icon="carbon:chevron-down"></i> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-1" title="Translator"> | |
| <i class="iconify" data-icon="carbon:operations-record"></i> | |
| <div class="relative"> | |
| <select class="w-9ch appearance-none bg-transparent border-b border-gray-300" v-model="translator"> | |
| <option v-for="key in validTranslators" :value="key">{{getTranslatorName(key)}}</option> | |
| </select> | |
| <i class="iconify absolute top-1.5 right-1 pointer-events-none" data-icon="carbon:chevron-down"></i> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-1" title="Target language"> | |
| <i class="iconify" data-icon="carbon:language"></i> | |
| <div class="relative"> | |
| <select class="w-15ch appearance-none bg-transparent border-b border-gray-300" v-model="targetLanguage"> | |
| <option value="CHS">简体中文</option> | |
| <option value="CHT">繁體中文</option> | |
| <option value="JPN">日本語</option> | |
| <option value="ENG">English</option> | |
| <option value="KOR">한국어</option> | |
| <option value="VIN">Tiếng Việt</option> | |
| <option value="CSY">čeština</option> | |
| <option value="NLD">Nederlands</option> | |
| <option value="FRA">français</option> | |
| <option value="DEU">Deutsch</option> | |
| <option value="HUN">magyar nyelv</option> | |
| <option value="ITA">italiano</option> | |
| <option value="PLK">polski</option> | |
| <option value="PTB">português</option> | |
| <option value="ROM">limba română</option> | |
| <option value="RUS">русский язык</option> | |
| <option value="ESP">español</option> | |
| <option value="TRK">Türk dili</option> | |
| <option value="IND">Indonesia</option> | |
| </select> | |
| <i class="iconify absolute top-1.5 right-1 pointer-events-none" data-icon="carbon:chevron-down"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div v-if="result" class="flex flex-col items-center"> | |
| <img class="my-2" :src="resultUri" /> | |
| <button class="px-2 py-1 text-center rounded-md text-blue-800 border-2 border-blue-300" @click="clear">Upload another</button> | |
| </div> | |
| <div v-else-if="status" class="grid w-full h-116 place-content-center rounded-2xl border-2 border-dashed border-gray-600"> | |
| <div v-if="error" class="flex flex-col items-center gap-2"> | |
| <div style="color: crimson">{{ statusText }}</div> | |
| <button class="px-2 py-1 text-center rounded-md text-blue-800 border-2 border-blue-300" @click="clear">Upload another</button> | |
| </div> | |
| <div v-else class="flex flex-col items-center gap-2"> | |
| <i class="iconify w-8 h-8 text-gray-500 animate-spin" data-icon="carbon:progress-bar-round"></i> | |
| <div>{{ statusText }}</div> | |
| </div> | |
| </div> | |
| <label | |
| v-else | |
| class="grid w-full h-116 place-content-center rounded-2xl border-2 border-dashed border-gray-600 cursor-pointer" | |
| for="file" | |
| @dragenter.prevent | |
| @dragover.prevent | |
| @dragleave.prevent | |
| @drop.prevent="ondrop" | |
| > | |
| <div v-if="file" class="flex flex-col items-center gap-2"> | |
| <div><span class="iconify-inline inline-block mr-2 scale-125" data-icon="carbon:image-search"></span>File Preview</div> | |
| <img class="max-w-72 max-h-72" :src="fileUri" /> | |
| <button type="submit" class="px-2 py-1 rounded-md text-blue-800 border-2 border-blue-300">Translate</button> | |
| <div class="text-sm text-gray-600">Click the empty space or paste/drag a new one to replace</div> | |
| </div> | |
| <div v-else class="flex flex-col items-center gap-2"> | |
| <i class="iconify w-8 h-8 text-gray-500" data-icon="carbon:cloud-upload"></i> | |
| <div>Paste an image, click to select one or drag and drop here</div> | |
| </div> | |
| <input id="file" type="file" accept="image/png,image/jpeg,image/bmp,image/webp" class="hidden" @change="onfilechange" /> | |
| </label> | |
| <div class="flex justify-center gap-2"> | |
| <div> | |
| Please consider supporting us by | |
| <a class="underline underline-blue-400" href="https://ko-fi.com/voilelabs" target="_blank" rel="noopener noreferrer">Ko-fi</a> | |
| or | |
| <a class="underline underline-blue-400" href="https://www.patreon.com/voilelabs" target="_blank" rel="noopener noreferrer" | |
| >Patreon</a | |
| >! | |
| </div> | |
| <a | |
| class="underline underline-blue-400" | |
| href="https://github.com/zyddnys/manga-image-translator" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| >Source Code</a | |
| > | |
| </div> | |
| </div> | |
| </form> | |
| <script> | |
| const BASE_URI = '/' | |
| const acceptTypes = ['image/png', 'image/jpeg', 'image/bmp', 'image/webp'] | |
| function formatSize(bytes) { | |
| const k = 1024 | |
| const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] | |
| if (bytes === 0) return '0B' | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)) | |
| return `${(bytes / k ** i).toFixed(2)}${sizes[i]}` | |
| } | |
| function formatProgress(loaded, total) { | |
| return `${formatSize(loaded)}/${formatSize(total)}` | |
| } | |
| PetiteVue.createApp({ | |
| onmounted() { | |
| window.addEventListener('paste', this.onpaste) | |
| }, | |
| file: null, | |
| get fileUri() { | |
| return this.file ? URL.createObjectURL(this.file) : null | |
| }, | |
| detectionResolution: 'M', | |
| textDetector: 'auto', | |
| renderTextDirection: 'auto', | |
| translator: 'youdao', | |
| validTranslators: ['youdao', 'baidu', 'google', 'deepl', 'papago', 'caiyun', 'offline', 'gpt3.5', 'none'], | |
| getTranslatorName(key) { | |
| if (key == 'none') | |
| return "No Text" | |
| return key ? key[0].toUpperCase() + key.slice(1) : ""; | |
| }, | |
| targetLanguage: 'CHS', | |
| ondrop(e) { | |
| const file = e.dataTransfer?.files?.[0] | |
| if (file && acceptTypes.includes(file.type)) { | |
| this.file = file | |
| } | |
| }, | |
| onfilechange(e) { | |
| const file = e.target.files?.[0] | |
| if (file && acceptTypes.includes(file.type)) { | |
| this.file = file | |
| } | |
| }, | |
| onpaste(e) { | |
| const items = (e.clipboardData || e.originalEvent.clipboardData).items | |
| for (const item of items) { | |
| if (item.kind === 'file') { | |
| const file = item.getAsFile() | |
| if (!file || !acceptTypes.includes(file.type)) continue | |
| this.file = file | |
| } | |
| } | |
| }, | |
| progress: null, | |
| status: null, | |
| queuePos: null, | |
| cachedStatusText: '', | |
| get statusText() { | |
| var newStatusText = this._statusText | |
| if (newStatusText != null && newStatusText != this.cachedStatusText) { | |
| this.cachedStatusText = newStatusText | |
| } | |
| return this.cachedStatusText | |
| }, | |
| get _statusText() { | |
| switch (this.status) { | |
| case 'upload': { | |
| if (this.progress) { | |
| return `Uploading (${this.progress})` | |
| } else { | |
| return 'Uploading' | |
| } | |
| } | |
| case 'download': | |
| if (this.progress) { | |
| return `Downloading (${this.progress})` | |
| } else { | |
| return 'Downloading' | |
| } | |
| case 'pending': | |
| if (this.queuePos) { | |
| return `Queuing, your position is ${this.queuePos}` | |
| } else { | |
| return 'Processing' | |
| } | |
| case 'detection': | |
| return 'Detecting texts' | |
| case 'ocr': | |
| return 'Running OCR' | |
| case 'mask-generation': | |
| return 'Generating text mask' | |
| case 'inpainting': | |
| return 'Running inpainting' | |
| case 'upscaling': | |
| return 'Running upscaling' | |
| case 'translating': | |
| return 'Translating' | |
| case 'rendering': | |
| return 'Rendering translated texts' | |
| case 'error': | |
| return 'Something went wrong, please try again' | |
| case 'error-upload': | |
| return 'Upload failed, please try again' | |
| case 'error-lang': | |
| return 'Your target language is not supported by the chosen translator' | |
| case 'error-translating': | |
| return 'Did not get any text back from the text translation service' | |
| case 'error-too-large': | |
| return 'Image size too large (greater than 8000x8000 px)' | |
| case 'error-disconnect': | |
| return 'Lost connection to server' | |
| } | |
| }, | |
| get error() { | |
| return /^error/.test(this.status) | |
| }, | |
| result: null, | |
| get resultUri() { | |
| return this.result ? URL.createObjectURL(this.result) : null | |
| }, | |
| onsubmit(e) { | |
| if (!this.file) return | |
| this.progress = null | |
| this.queuePos = null | |
| this.status = 'upload' | |
| const formData = new FormData() | |
| formData.append('file', this.file) | |
| formData.append('size', this.detectionResolution) | |
| formData.append('detector', this.textDetector) | |
| formData.append('direction', this.renderTextDirection) | |
| formData.append('translator', this.translator) | |
| formData.append('tgt_lang', this.targetLanguage) | |
| const xhr = new XMLHttpRequest() | |
| xhr.open('POST', `${BASE_URI}submit`, true) | |
| xhr.onerror = (e) => { | |
| this.status = 'error-disconnect' | |
| } | |
| xhr.upload.onprogress = (e) => { | |
| if (e.lengthComputable) this.progress = formatProgress(e.loaded, e.total) | |
| } | |
| xhr.onload = async () => { | |
| if (xhr.status !== 200) { | |
| this.status = 'error-upload' | |
| return | |
| } | |
| response = JSON.parse(xhr.responseText) | |
| const task_id = response['task_id'] | |
| this.status = response['status'] | |
| if (this.error) | |
| return | |
| this.status = 'pending' | |
| async function tryFetchTaskState() { | |
| try { | |
| return await (await fetch(`${BASE_URI}task-state?taskid=${task_id}`)).json() | |
| } | |
| catch { | |
| return null | |
| } | |
| } | |
| for (;;) { | |
| const timer = new Promise((resolve) => setTimeout(resolve, 500)) | |
| const res = await tryFetchTaskState() | |
| if (res == null) { | |
| this.status = 'error-disconnect' | |
| break | |
| } | |
| const { state, finished, waiting } = res | |
| // console.log(state, finished, waiting) | |
| if (finished && !state.startsWith('error')) { | |
| this.progress = null | |
| this.status = 'download' | |
| const xhrDownload = new XMLHttpRequest() | |
| xhrDownload.open('GET', `${BASE_URI}result/${task_id}`, true) | |
| xhrDownload.responseType = 'blob' | |
| xhrDownload.onprogress = (e) => { | |
| if (e.lengthComputable) this.progress = formatProgress(e.loaded, e.total) | |
| } | |
| xhrDownload.onload = () => { | |
| this.result = xhrDownload.response | |
| this.status = null | |
| } | |
| xhrDownload.send() | |
| break | |
| } | |
| this.status = state | |
| this.queuePos = waiting | |
| if (/^error/.test(state)) { | |
| break | |
| } | |
| await timer | |
| } | |
| } | |
| xhr.send(formData) | |
| }, | |
| clear() { | |
| this.file = null | |
| this.result = null | |
| this.status = null | |
| }, | |
| }).mount() | |
| </script> | |
| </body> | |
| </html> | |