diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 9ca80c47..cb039be8 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -88,6 +88,16 @@ Downloaded from their official release pages when the user chooses to install th | Tesseract language data (tessdata_fast 4.1.0), downloaded per language when used | Apache-2.0 | https://github.com/tesseract-ocr/tessdata_fast | | whisper.cpp (whisper-cli.exe Windows build) | MIT | https://github.com/ggml-org/whisper.cpp | | ggml Whisper base model (`ggml-base.bin`) | MIT | https://huggingface.co/ggerganov/whisper.cpp | +| upscayl-bin (AI super-resolution binary) | AGPL-3.0 | https://github.com/upscayl/upscayl-ncnn | +| NMKD Siax (`4x_NMKD-Siax_200k`, nmkd) | WTFPL | https://github.com/upscayl/custom-models | +| NMKD Superscale (`4x_NMKD-Superscale-SP_178000_G`, nmkd) | WTFPL | https://github.com/upscayl/custom-models | +| RealESRGAN General (`RealESRGAN_General_x4_v3`, Xintao Wang) | BSD-3-Clause | https://github.com/xinntao/Real-ESRGAN | +| RealESRGAN General WDN (`RealESRGAN_General_WDN_x4_v3`, Xintao Wang) | BSD-3-Clause | https://github.com/xinntao/Real-ESRGAN | +| 4xLSDIR (`4xLSDIR`, Philip Hofmann / Phhofm) | CC-BY-4.0 | https://github.com/upscayl/custom-models, https://openmodeldb.info/models/4x-LSDIR | +| 4xLSDIRplusC (`4xLSDIRplusC`, Philip Hofmann / Phhofm) | CC-BY-4.0 | https://github.com/upscayl/custom-models, https://openmodeldb.info/models/4x-LSDIRplusC | +| 4xLSDIRCompactC3 (`4xLSDIRCompactC3`, Philip Hofmann / Phhofm) | CC-BY-4.0 | https://github.com/upscayl/custom-models, https://openmodeldb.info/models/4x-LSDIRCompactC3 | +| 4xNomos8kSC (`4xNomos8kSC`, Philip Hofmann / Phhofm) | CC-BY-4.0 | https://github.com/upscayl/custom-models, https://openmodeldb.info/models/4x-Nomos8kSC | +| 4xHFA2k (`4xHFA2k`, Philip Hofmann / Phhofm) | CC-BY-4.0 | https://github.com/upscayl/custom-models, https://openmodeldb.info/models/4x-HFA2k | These programs run as separate processes. Their source code is available from the linked projects. diff --git a/docs/ENGINES.md b/docs/ENGINES.md index 7789e22a..a88700ac 100644 --- a/docs/ENGINES.md +++ b/docs/ENGINES.md @@ -1911,3 +1911,17 @@ Filee translates documents, books, and text files locally using a local Ollama i - **Documents & E-books**: DOCX, HWPX, EPUB, KEPUB, FB2, HTMLZ, TXTZ, MD, TXT, and HTML are translated in place, preserving document structure, headings, bold/italic runs, tables, lists, images, and notes. - **Kindle e-books**: MOBI, AZW, AZW3, and PRC formats are translated via EPUB, and can then be routed to any supported target format. - **PDF target**: When a document is translated with PDF as the target format, the route planner translates the document model first and then routes the translated document through Filee's standard document-to-PDF export pipeline. + +## Image Quality Models (Offline AI Upscaler) + +Filee upscales and restores images locally using an offline AI super-resolution engine based on Vulkan/NCNN (`upscayl-bin` / `upscayl-ncnn`) and curated open-source neural network models (`upscayl/custom-models`). All processing runs 100% offline on your computer; no image data is ever uploaded or sent to external servers. + +- **Offline privacy & local execution**: Like all Filee features, image upscaling processes every pixel locally on your machine via direct Vulkan GPU acceleration. No network connection is used during upscaling. Requires a Vulkan-compatible graphics device. +- **Models catalog**: Curated selection of high-quality image models for photos, digital art, illustration, and restoration: + - Fast & lightweight models (integrated and entry-level GPUs): `4xLSDIRCompactC3`, `RealESRGAN_General_x4_v3`, `RealESRGAN_General_WDN_x4_v3`. + - Detailed & high-fidelity models (recommended for dedicated GPUs): `4x_NMKD-Siax_200k`, `4x_NMKD-Superscale-SP_178000_G`, `4xLSDIR`, `4xLSDIRplusC`, `4xNomos8kSC`, `4xHFA2k`. +- **Where it lives**: + - **Palette**: The **Upscale** preset opens the interactive Upscale dialog to select models, scale factors (2× to 4×), preview output resolution, and upscale directly. + - **Settings → Presets**: The "Upscale Image" tool preset can be configured with preferred default models and scale factors. + - **Settings → Engines**: "Upscayl (AI image upscaler)" engine package provides the local `upscayl-bin` binary, with models downloaded on demand with live progress and uninstall support in the Upscale dialog. +- **Supported formats**: PNG, JPG, WEBP, ICO, HEIC, AVIF, BMP, and TIFF. diff --git a/src/Filee.App/Assets/i18n/ar.json b/src/Filee.App/Assets/i18n/ar.json index 8995fedf..ed1c00df 100644 --- a/src/Filee.App/Assets/i18n/ar.json +++ b/src/Filee.App/Assets/i18n/ar.json @@ -17,7 +17,6 @@ "fields.name.video-pro": "إنتاج الفيديو والبث", "fields.name.audio-pro": "الصوت الاحترافي", "fields.name.legacy": "الصيغ القديمة", - "nav.home": "الرئيسية", "nav.general": "عام", "nav.triggers": "الاختصارات", @@ -734,17 +733,17 @@ "toolbar.mixed_covered": "يأتي «{0}» أولًا ويسمح بكل الامتدادات المحددة هنا، لذا لن يُستخدم ملف التعريف هذا.", "toolbar.mixed_others": "تفتح التحديدات المختلطة الأخرى «{0}».", "toolbar.mixed_others_self": "لا يوجد ملف تعريف مختلط بلا امتدادات محددة، لذا يُستخدم هذا الملف أيضًا لكل مزيج آخر.", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ التنسيق نفسه", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "الصيغ المقبولة: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": "، ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} · Shift لعرض الكل", - "toolbar.chip_tip.shift_all": "Shift لعرض الكل", - "toolbar.chip_tip.any_file": "أي ملف", - "tags.remove": "إزالة", + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ التنسيق نفسه", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "الصيغ المقبولة: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": "، ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} · Shift لعرض الكل", + "toolbar.chip_tip.shift_all": "Shift لعرض الكل", + "toolbar.chip_tip.any_file": "أي ملف", + "tags.remove": "إزالة", "presets.title": "الإعدادات المسبقة", "presets.add": "جديد", "presets.add_empty": "إعداد مسبق فارغ", @@ -1414,7 +1413,6 @@ "sheet_studio.apply_fix": "تطبيق التخطيط المقترح", "sheet_studio.add_region": "+ إضافة نطاق آخر", "sheet_studio.region_name": "{0} · النطاق {1}", - "whats_new.title": "الجديد في Filee {0}", "whats_new.previous": "السابق", "whats_new.next": "التالي", @@ -1492,6 +1490,28 @@ "engines.ollama.complete": "اكتمل!", "engines.ollama.failed": "فشل:", "engines.ollama.error": "خطأ:", + "preset.upscale": "ترقية جودة الصورة", + "preset.upscale.donut": "ترقية الجودة", + "engine.upscaler.name": "نماذج جودة الصورة", + "engine.upscaler.description": "ترقية دقة الصور بالذكاء الاصطناعي حتى 4 أضعاف باستخدام نماذج NCNN المحلية.", + "engines.upscaler.modal_description": "قم بتنزيل نماذج الذكاء الاصطناعي المحلية لترقية دقة الصور حتى 4×. تجري كافة العمليات دون اتصال بالإنترنت بنسبة 100%.", + "upscale.model": "النموذج", + "upscale.scale": "معامل التكبير", + "upscale.output_format": "تنسيق الإخراج", + "upscale.format_same": "نفس التنسيق الأصلي ({0})", + "upscale.resolution_label": "معاينة الدقة", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "ترقية الآن", + "upscale.success": "اكتملت ترقية الصورة بنجاح.", + "error.upscale_failed": "فشلت الترقية: {0}", + "engines.package.upscayl-bin.name": "Upscayl (ترقية دقة الصور بالذكاء الاصطناعي)", + "engines.package.upscayl-bin.description": "ترقية دقة الصور والدقة الفائقة حتى 4 أضعاف بنماذج NCNN محلية.\nتُنزَّل النماذج عند استخدامها أول مرة.", + "error.upscale_needs_engine": "ثبّت Upscayl من الإعدادات ← المحركات لاستخدام ترقية الدقة بالذكاء الاصطناعي.", + "error.upscale_needs_vulkan": "يتطلب رفع دقة الصور بطاقة رسومات (GPU) متوافقة مع Vulkan.", + "upscale.model_ready": "جاهز", + "upscale.model_download_size": "تنزيل {0} م.ب", + "upscale.downloading_model": "جارٍ تنزيل النموذج...", + "engines.model.delete_confirm": "هل تريد حذف النموذج {0}؟ يمكنك تنزيله مرة أخرى في أي وقت.", "home.queue_error": "تعذر تحويل {0}. تحتفظ القائمة بالملفات.", "home.queue_selected": "محدد", "home.queue_remove_file": "إزالة {0}", diff --git a/src/Filee.App/Assets/i18n/de.json b/src/Filee.App/Assets/i18n/de.json index 75bbfc5e..bf421233 100644 --- a/src/Filee.App/Assets/i18n/de.json +++ b/src/Filee.App/Assets/i18n/de.json @@ -17,7 +17,6 @@ "fields.name.video-pro": "Videoproduktion und Rundfunk", "fields.name.audio-pro": "Professionelles Audio", "fields.name.legacy": "Alte Formate", - "nav.home": "Start", "nav.general": "Allgemein", "nav.triggers": "Tastenkürzel", @@ -734,17 +733,17 @@ "toolbar.mixed_covered": "„{0}“ steht weiter oben und erlaubt alle hier ausgewählten Dateiendungen. Dieses Profil wird daher nie verwendet.", "toolbar.mixed_others": "Andere Dateimischungen öffnen „{0}“.", "toolbar.mixed_others_self": "Kein Profil für gemischte Auswahlen ist ohne Auswahl geblieben. Dieses Profil wird daher auch für alle anderen Dateimischungen verwendet.", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ Gleiches Format", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "Formate: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} zusätzlich · Shift zeigt alle", - "toolbar.chip_tip.shift_all": "Shift zeigt alle", - "toolbar.chip_tip.any_file": "Beliebige Datei", - "tags.remove": "Entfernen", + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ Gleiches Format", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "Formate: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} zusätzlich · Shift zeigt alle", + "toolbar.chip_tip.shift_all": "Shift zeigt alle", + "toolbar.chip_tip.any_file": "Beliebige Datei", + "tags.remove": "Entfernen", "presets.title": "Vorlagen", "presets.add": "Neu", "presets.add_empty": "Leere Vorlage", @@ -1491,6 +1490,28 @@ "engines.ollama.complete": "Abgeschlossen!", "engines.ollama.failed": "Fehlgeschlagen:", "engines.ollama.error": "Fehler:", + "preset.upscale": "Bild hochskalieren", + "preset.upscale.donut": "Hochskalieren", + "engine.upscaler.name": "Bildqualitäts-Modelle", + "engine.upscaler.description": "KI-Bild-Hochskalierung und Super-Resolution bis zu 4× mit lokalen NCNN-Modellen.", + "engines.upscaler.modal_description": "Laden Sie lokale KI-Modelle herunter, um Bilder bis zu 4× hochzuskalieren. Die Verarbeitung erfolgt zu 100 % offline auf Ihrem Gerät.", + "upscale.model": "Modell", + "upscale.scale": "Skalierungsfaktor", + "upscale.output_format": "Ausgabeformat", + "upscale.format_same": "Gleiches Format wie Quelle ({0})", + "upscale.resolution_label": "Auflösungsvorschau", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "Jetzt hochskalieren", + "upscale.success": "Hochskalierung erfolgreich abgeschlossen.", + "error.upscale_failed": "Hochskalierung fehlgeschlagen: {0}", + "engines.package.upscayl-bin.name": "Upscayl (KI-Bild-Upscaler)", + "engines.package.upscayl-bin.description": "KI-Bild-Upscaling und Super-Resolution bis zu 4× mit lokalen NCNN-Modellen.\nModelle werden bei der ersten Verwendung heruntergeladen.", + "error.upscale_needs_engine": "Installiere Upscayl unter Einstellungen → Module, um KI-Upscaling zu nutzen.", + "error.upscale_needs_vulkan": "Für das Hochskalieren ist eine Vulkan-kompatible Grafikkarte (GPU) erforderlich.", + "upscale.model_ready": "Bereit", + "upscale.model_download_size": "{0} MB Download", + "upscale.downloading_model": "Modell wird heruntergeladen...", + "engines.model.delete_confirm": "Modell {0} löschen? Du kannst es jederzeit wieder herunterladen.", "home.queue_error": "{0} konnte nicht konvertiert werden. Die Liste behält die Dateien.", "home.queue_selected": "ausgewählt", "home.queue_remove_file": "{0} entfernen", diff --git a/src/Filee.App/Assets/i18n/en.json b/src/Filee.App/Assets/i18n/en.json index 23877fcd..9784b833 100644 --- a/src/Filee.App/Assets/i18n/en.json +++ b/src/Filee.App/Assets/i18n/en.json @@ -1,7 +1,6 @@ { "app.name": "Filee", "app.tagline": "Drag, drop, converted.", - "fields.title": "Fields", "fields.description": "Turn off the fields you don't work in. A format that belongs only to fields that are off is left out of the donut's slices, “Available presets” in Settings → Donut toolbar, the list in Settings → Presets and a preset's “Convert to” list. Filee still recognizes its files and the command line still converts to it, and a file you drop still offers the formats of its own fields.", "fields.formats": "Formats: {0}", @@ -18,7 +17,6 @@ "fields.name.video-pro": "Video production & broadcast", "fields.name.audio-pro": "Pro audio", "fields.name.legacy": "Old formats", - "nav.home": "Home", "nav.general": "General", "nav.triggers": "Shortcuts", @@ -31,7 +29,6 @@ "nav.about": "About", "nav.collapse": "Collapse sidebar", "nav.expand": "Expand sidebar", - "tray.open": "Open Filee", "tray.pause": "Pause shortcuts", "tray.resume": "Resume shortcuts", @@ -43,7 +40,6 @@ "close.tray": "Keep in tray", "close.quit": "Quit", "close.dont_ask": "Don't ask again", - "home.title": "Home", "home.drop_title": "Drop files here", "home.drop_select": "Double-click to select files", @@ -72,7 +68,6 @@ "home.undone": "Undone: the results are in the Recycle Bin / Trash", "home.undone_original": "Undone: the results are in the Recycle Bin / Trash, and so are the originals they replaced", "home.undo_failed": "{0} couldn't be moved to the Recycle Bin / Trash: {1}", - "donut.center_drag": "Bring files here", "donut.files_same": "{0} × {1}", "donut.files_mixed": "{0} files", @@ -88,7 +83,6 @@ "donut.group_caption": "Group of {0}", "donut.flip_to_back": "{0} back", "donut.flip_to_front": "{0} front", - "toast.converting": "{0} → {1}", "toast.done": "Done", "toast.done_errors": "{0} of {1} failed", @@ -101,7 +95,6 @@ "toast.get_fonts": "Get fonts", "download.badge": "{0}/s · {1}", "download.badge_files": "{0} · {1} of {2} files", - "error.unsupported_source": "This file type isn't supported", "error.empty_file": "The file is empty (0 bytes). A file made with “New” in the right-click menu stays empty until you open it in its app and save it", "error.p7m_unreadable": "This isn't a signed .p7m file Filee can open, or it is damaged", @@ -194,7 +187,6 @@ "notice.geo_field_names": "Shapefile field names are 10 characters at most: some were shortened", "notice.geo_shapefile_split": "A Shapefile holds one kind of shape, so points, lines and polygons were saved as separate files", "notice.geo_heights_dropped": "Some points have no height and a Shapefile can't leave one out, so all heights were left out", - "engine.reason.not_installed": "Not installed", "engine.reason.error": "Error while checking", "engine.reason.unsupported_platform": "Not available on this platform", @@ -257,7 +249,6 @@ "engine.calibre.description": "Rare e-book formats (LIT, LRF, CHM, PDB, RB…) and Kindle output (MOBI, AZW3).\nAbove the HWPX writer, it also makes the PDFs of e-books (much faster for long books).", "engine.libreoffice.description": "Reads older and less common formats: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nAlso saves as DOC, XLS, PPT, ODT, ODP, OpenDocument templates and EMF.", "engine.translator.description": "Translates documents and e-books locally using offline Ollama models.\nAlso translates SRT and WebVTT subtitles: only the cue text is sent to the model, cue numbers and timestamps are kept.", - "profile.images": "Images", "profile.pdf": "PDF", "profile.office": "Documents", @@ -277,7 +268,6 @@ "profile.models": "3D models", "profile.geo": "Geospatial", "profile.mixed": "Mixed files", - "preset.crop": "Crop", "crop.save": "Crop & Save", "crop.reset": "Reset", @@ -404,7 +394,6 @@ "clip.silence_removed": "Silence removed: {0}", "clip.no_silence": "No silence found", "clip.cannot_play": "The clip can't be played", - "preset.half_size": "Half size", "preset.half_size.donut": "Half size", "preset.image_edit": "Edit pictures", @@ -519,7 +508,6 @@ "preset.merge_audio": "Merge audio", "preset.merge_images": "Merge images", "preset.remove_metadata": "Remove metadata", - "format.folder": "Folder", "format.category.Image": "Image", "format.category.Pdf": "PDF", @@ -539,7 +527,6 @@ "format.category.Data": "Data", "format.category.Model3D": "3D model", "format.category.Geo": "Geospatial", - "general.title": "General", "general.language": "Language", "general.language_auto": "System default", @@ -622,7 +609,6 @@ "general.reset_library": "Reset to defaults", "general.reset_confirm": "Replace all presets and toolbar profiles with the defaults?", "general.imported": "Imported {0} items", - "triggers.title": "Shortcuts", "triggers.description": "Choose how the donut toolbar opens. You can add as many gestures as you like.", "triggers.macos_permissions": "Allow Filee in System Settings > Privacy & Security > Accessibility.\nIf Filee is already switched on but gestures don't start, remove it with − and add it again with +.\nFinder also asks for Automation permission the first time you use a selection shortcut.", @@ -680,7 +666,6 @@ "triggers.excluded_none_running": "No other app has a window open.", "triggers.excluded_note": "Gestures and shortcuts do nothing while the pointer is over these apps.", "triggers.paused": "Shortcuts are paused", - "toolbar.title": "Donut toolbar", "toolbar.step_profile": "1. Choose a profile", "toolbar.step_arrange": "2. Arrange presets", @@ -748,18 +733,17 @@ "toolbar.mixed_covered": "“{0}” comes first and allows every extension checked here, so this profile is never used.", "toolbar.mixed_others": "Other mixes of files open “{0}”.", "toolbar.mixed_others_self": "No mixed profile is left with nothing checked, so this one is also used for every other mix.", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ Same format", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "Accepted formats: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} more · Shift to show all", - "toolbar.chip_tip.shift_all": "Shift to show all", - "toolbar.chip_tip.any_file": "Any file", - "tags.remove": "Remove", - + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ Same format", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "Accepted formats: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} more · Shift to show all", + "toolbar.chip_tip.shift_all": "Shift to show all", + "toolbar.chip_tip.any_file": "Any file", + "tags.remove": "Remove", "presets.title": "Presets", "presets.add": "New", "presets.add_empty": "Empty preset", @@ -1083,7 +1067,6 @@ "presets.delete_confirm": "Delete “{0}”? It will also be removed from every toolbar and group.", "presets.name_required": "Enter a name", "presets.select": "Select a preset to edit it", - "theme.title": "Theme", "theme.style": "Theme style", "theme.style.Soft": "Filee Soft", @@ -1120,7 +1103,6 @@ "theme.preview": "Preview", "theme.preview_toggle": "Sample switch (preview only, not a setting)", "theme.preview_toggle_hint": "Shows how switches look in this theme. It doesn't change any setting.", - "engines.title": "Conversion engines", "engines.description": "Filee picks the best available engine for each conversion.\nWhen several can do the same job, the higher one wins.", "engines.available": "Ready", @@ -1197,7 +1179,6 @@ "engines.state.unpacking": "Installing…", "engines.state.installed": "Installed", "engines.state.failed": "Failed: {0}", - "about.title": "About", "about.updates": "Updates", "common.search": "Search", @@ -1222,7 +1203,6 @@ "feedback.notice_text": "A star on GitHub helps other people find Filee.\nProblems and ideas are welcome too.", "feedback.star": "Star on GitHub", "feedback.issue": "Report a problem or idea", - "common.ok": "OK", "common.cancel": "Cancel", "common.save": "Save", @@ -1232,7 +1212,6 @@ "common.no": "No", "common.on": "On", "common.off": "Off", - "setup.title": "Choose conversion engines", "setup.description": "Images, PDF, Office documents, HWP, e-books, archives, fonts, CAD and 3D models already convert.\nDownload engines only for video and audio or rare formats.", "setup.note": "You can install or remove engines at any time in Settings → Engines.", @@ -1395,7 +1374,6 @@ "watch.status_starting": "Starting…", "watch.status_error": "Can't watch this folder: {0}", "watch.cli_hint": "Also on the command line: filee watch --to ", - "preset.sheet_studio": "PDF + Edit", "sheet_studio.title": "Spreadsheet Print Studio", "sheet_studio.sheets": "Sheets and output order", @@ -1435,7 +1413,6 @@ "sheet_studio.apply_fix": "Apply suggested layout", "sheet_studio.add_region": "+ Add another region", "sheet_studio.region_name": "{0} · region {1}", - "whats_new.title": "What's new in Filee {0}", "whats_new.previous": "Previous", "whats_new.next": "Next", @@ -1513,6 +1490,28 @@ "engines.ollama.complete": "Complete!", "engines.ollama.failed": "Failed:", "engines.ollama.error": "Error:", + "preset.upscale": "Upscale Image", + "preset.upscale.donut": "Upscale", + "engine.upscaler.name": "Image Quality Models", + "engine.upscaler.description": "AI image upscaling and super-resolution up to 4× with local NCNN models.", + "engines.upscaler.modal_description": "Download local AI super-resolution models to upscale images up to 4×. All processing runs 100% offline on your device.", + "upscale.model": "Model", + "upscale.scale": "Scale Factor", + "upscale.output_format": "Output Format", + "upscale.format_same": "Same format as source ({0})", + "upscale.resolution_label": "Resolution Preview", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "Upscale Now", + "upscale.success": "Upscaling completed successfully.", + "error.upscale_failed": "Upscaling failed: {0}", + "engines.package.upscayl-bin.name": "Upscayl (AI image upscaler)", + "engines.package.upscayl-bin.description": "AI image upscaling and super-resolution up to 4× with local NCNN models.\nModels download the first time they are used.", + "error.upscale_needs_engine": "Install Upscayl in Settings → Engines to use AI upscaling.", + "error.upscale_needs_vulkan": "Upscaling requires a Vulkan-compatible graphics card (GPU).", + "upscale.model_ready": "Ready", + "upscale.model_download_size": "{0} MB download", + "upscale.downloading_model": "Downloading model...", + "engines.model.delete_confirm": "Delete model {0}? You can download it again at any time.", "home.queue_error": "Could not convert {0}. The list keeps its files.", "home.queue_selected": "selected", "home.queue_remove_file": "Remove {0}", diff --git a/src/Filee.App/Assets/i18n/es.json b/src/Filee.App/Assets/i18n/es.json index add9a19f..28a26117 100644 --- a/src/Filee.App/Assets/i18n/es.json +++ b/src/Filee.App/Assets/i18n/es.json @@ -17,7 +17,6 @@ "fields.name.video-pro": "Producción de vídeo y radiodifusión", "fields.name.audio-pro": "Audio profesional", "fields.name.legacy": "Formatos antiguos", - "nav.home": "Inicio", "nav.general": "General", "nav.triggers": "Atajos", @@ -734,17 +733,17 @@ "toolbar.mixed_covered": "«{0}» va antes y admite todas las extensiones marcadas aquí, así que este perfil no se usa nunca.", "toolbar.mixed_others": "Las demás combinaciones de archivos abren «{0}».", "toolbar.mixed_others_self": "Ningún perfil mixto quedó sin nada marcado, así que este también se usa para todas las demás combinaciones.", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ Mismo formato", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "Formatos admitidos: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} más · Shift para ver todos", - "toolbar.chip_tip.shift_all": "Shift para ver todos", - "toolbar.chip_tip.any_file": "Cualquier archivo", - "tags.remove": "Quitar", + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ Mismo formato", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "Formatos admitidos: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} más · Shift para ver todos", + "toolbar.chip_tip.shift_all": "Shift para ver todos", + "toolbar.chip_tip.any_file": "Cualquier archivo", + "tags.remove": "Quitar", "presets.title": "Ajustes preestablecidos", "presets.add": "Nuevo", "presets.add_empty": "Ajuste vacío", @@ -1375,7 +1374,6 @@ "watch.status_starting": "Iniciando…", "watch.status_error": "No se puede supervisar esta carpeta: {0}", "watch.cli_hint": "También en la línea de comandos: filee watch --to ", - "preset.sheet_studio": "PDF + Editar", "sheet_studio.title": "Estudio de impresión de hojas de cálculo", "sheet_studio.sheets": "Hojas y orden de salida", @@ -1415,7 +1413,6 @@ "sheet_studio.apply_fix": "Aplicar diseño sugerido", "sheet_studio.add_region": "+ Añadir otra área", "sheet_studio.region_name": "{0} · área {1}", - "whats_new.title": "Novedades de Filee {0}", "whats_new.previous": "Anterior", "whats_new.next": "Siguiente", @@ -1493,6 +1490,28 @@ "engines.ollama.complete": "¡Completado!", "engines.ollama.failed": "Error:", "engines.ollama.error": "Error:", + "preset.upscale": "Escalar imagen", + "preset.upscale.donut": "Escalar", + "engine.upscaler.name": "Modelos de calidad de imagen", + "engine.upscaler.description": "Mejora y superresolución de imágenes con IA local hasta 4× mediante modelos NCNN.", + "engines.upscaler.modal_description": "Descarga modelos locales de superresolución para escalar imágenes hasta 4×. Todo el procesamiento se ejecuta 100% sin conexión.", + "upscale.model": "Modelo", + "upscale.scale": "Factor de escala", + "upscale.output_format": "Formato de salida", + "upscale.format_same": "Mismo formato que el origen ({0})", + "upscale.resolution_label": "Vista previa de resolución", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "Escalar ahora", + "upscale.success": "Escalado completado con éxito.", + "error.upscale_failed": "Error al escalar: {0}", + "engines.package.upscayl-bin.name": "Upscayl (escalador de imágenes por IA)", + "engines.package.upscayl-bin.description": "Escalado de imágenes y superresolución por IA hasta 4× con modelos NCNN locales.\nLos modelos se descargan la primera vez que se usan.", + "error.upscale_needs_engine": "Instala Upscayl en Ajustes → Motores para usar el escalado por IA.", + "error.upscale_needs_vulkan": "El escalado requiere una tarjeta gráfica (GPU) compatible con Vulkan.", + "upscale.model_ready": "Listo", + "upscale.model_download_size": "Descarga de {0} MB", + "upscale.downloading_model": "Descargando modelo...", + "engines.model.delete_confirm": "¿Eliminar el modelo {0}? Puedes descargarlo de nuevo en cualquier momento.", "home.queue_error": "No se pudo convertir {0}. La lista conserva los archivos.", "home.queue_selected": "seleccionado", "home.queue_remove_file": "Quitar {0}", diff --git a/src/Filee.App/Assets/i18n/fr.json b/src/Filee.App/Assets/i18n/fr.json index 4f92eb50..6a9288a9 100644 --- a/src/Filee.App/Assets/i18n/fr.json +++ b/src/Filee.App/Assets/i18n/fr.json @@ -1,7 +1,6 @@ { "app.name": "Filee", "app.tagline": "Glissez, déposez, c'est converti.", - "fields.title": "Domaines", "fields.description": "Désactivez les domaines dans lesquels vous ne travaillez pas. Un format appartenant uniquement à des domaines désactivés n'apparaît pas dans les tranches du menu circulaire, dans « Préréglages disponibles » de Paramètres → Menu circulaire, dans la liste de Paramètres → Préréglages ni dans la liste « Convertir en » d'un préréglage. Filee reconnaît toujours ses fichiers et la ligne de commande peut toujours convertir vers ce format ; un fichier déposé propose toujours les formats de ses propres domaines.", "fields.formats": "Formats : {0}", @@ -18,7 +17,6 @@ "fields.name.video-pro": "Production vidéo et diffusion", "fields.name.audio-pro": "Audio professionnel", "fields.name.legacy": "Anciens formats", - "nav.home": "Accueil", "nav.general": "Général", "nav.triggers": "Raccourcis", @@ -31,7 +29,6 @@ "nav.about": "À propos", "nav.collapse": "Réduire la barre latérale", "nav.expand": "Développer la barre latérale", - "tray.open": "Ouvrir Filee", "tray.pause": "Suspendre les raccourcis", "tray.resume": "Réactiver les raccourcis", @@ -43,7 +40,6 @@ "close.tray": "Garder en arrière-plan", "close.quit": "Quitter", "close.dont_ask": "Ne plus demander", - "home.title": "Accueil", "home.drop_title": "Déposez des fichiers ici", "home.drop_select": "Double-cliquez pour sélectionner des fichiers", @@ -72,7 +68,6 @@ "home.undone": "Annulé : les résultats sont dans la Corbeille", "home.undone_original": "Annulé : les résultats sont dans la Corbeille, tout comme les originaux qu'ils avaient remplacés", "home.undo_failed": "Impossible de déplacer {0} dans la Corbeille : {1}", - "donut.center_drag": "Amenez les fichiers ici", "donut.files_same": "{0} × {1}", "donut.files_mixed": "{0} fichiers", @@ -88,7 +83,6 @@ "donut.group_caption": "{0} options", "donut.flip_to_back": "{0} verso", "donut.flip_to_front": "{0} recto", - "toast.converting": "{0} → {1}", "toast.done": "Terminé", "toast.done_errors": "{0} sur {1} en échec", @@ -101,7 +95,6 @@ "toast.get_fonts": "Obtenir les polices", "download.badge": "{0}/s · {1}", "download.badge_files": "{0} · {1} sur {2} fichiers", - "error.unsupported_source": "Ce type de fichier n'est pas pris en charge", "error.empty_file": "Le fichier est vide (0 octet). Un fichier créé avec « Nouveau » dans le menu contextuel reste vide tant que vous ne l'avez pas ouvert et enregistré dans son application", "error.p7m_unreadable": "Ce n'est pas un fichier signé .p7m que Filee peut ouvrir, ou il est endommagé", @@ -194,7 +187,6 @@ "notice.geo_field_names": "Les noms de champs Shapefile font 10 caractères au maximum : certains ont été raccourcis", "notice.geo_shapefile_split": "Un Shapefile ne contient qu'un type de forme : les points, lignes et polygones ont été enregistrés dans des fichiers distincts", "notice.geo_heights_dropped": "Certains points n'ont pas de hauteur et Shapefile ne peut pas en omettre une seule : toutes les hauteurs ont donc été omises", - "engine.reason.not_installed": "Non installé", "engine.reason.error": "Erreur lors de la vérification", "engine.reason.unsupported_platform": "Non disponible sur cette plateforme", @@ -257,7 +249,6 @@ "engine.calibre.description": "Formats de livres numériques rares (LIT, LRF, CHM, PDB, RB…) et sortie Kindle (MOBI, AZW3).\nPlacé au-dessus du rédacteur HWPX, il produit aussi les PDF des livres numériques (bien plus vite pour les longs livres).", "engine.libreoffice.description": "Lit les formats anciens et moins courants : DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nEnregistre aussi en DOC, XLS, PPT, ODT, ODP, modèles OpenDocument et EMF.", "engine.translator.description": "Traduit les documents et livres électroniques localement à l'aide de modèles Ollama hors ligne.\nTraduit aussi les sous-titres SRT et WebVTT : seul le texte des répliques est envoyé au modèle, les numéros et les horodatages sont conservés.", - "profile.images": "Images", "profile.pdf": "PDF", "profile.office": "Documents", @@ -277,7 +268,6 @@ "profile.models": "Modèles 3D", "profile.geo": "Géospatial", "profile.mixed": "Fichiers mixtes", - "preset.crop": "Recadrer", "crop.save": "Recadrer et enregistrer", "crop.reset": "Réinitialiser", @@ -404,7 +394,6 @@ "clip.silence_removed": "Silences retirés : {0}", "clip.no_silence": "Aucun silence trouvé", "clip.cannot_play": "Impossible de lire l'extrait", - "preset.half_size": "Taille 50 %", "preset.half_size.donut": "Taille 50 %", "preset.image_edit": "Modifier les images", @@ -519,7 +508,6 @@ "preset.merge_audio": "Fusionner l'audio", "preset.merge_images": "Fusionner les images", "preset.remove_metadata": "Supprimer les métadonnées", - "format.folder": "Dossier", "format.category.Image": "Image", "format.category.Pdf": "PDF", @@ -539,7 +527,6 @@ "format.category.Data": "Données", "format.category.Model3D": "Modèle 3D", "format.category.Geo": "Géospatial", - "general.title": "Général", "general.language": "Langue", "general.language_auto": "Langue du système", @@ -622,7 +609,6 @@ "general.reset_library": "Rétablir les valeurs par défaut", "general.reset_confirm": "Remplacer tous les préréglages et profils de menu circulaire par ceux par défaut ?", "general.imported": "{0} éléments importés", - "triggers.title": "Raccourcis", "triggers.description": "Choisissez comment s'ouvre le menu circulaire. Vous pouvez ajouter autant de gestes que vous voulez.", "triggers.macos_permissions": "Autorisez Filee dans Réglages Système > Confidentialité et sécurité > Accessibilité.\nSi Filee est déjà activé mais que les gestes ne démarrent pas, supprimez-le avec − puis ajoutez-le à nouveau avec +.\nLe Finder demande aussi l'autorisation d'automatisation la première fois que vous utilisez un raccourci sur la sélection.", @@ -680,7 +666,6 @@ "triggers.excluded_none_running": "Aucune autre application n'a de fenêtre ouverte.", "triggers.excluded_note": "Les gestes et raccourcis sont sans effet quand le pointeur survole ces applications.", "triggers.paused": "Raccourcis en pause", - "toolbar.title": "Menu circulaire", "toolbar.step_profile": "1. Choisir un profil", "toolbar.step_arrange": "2. Organiser les préréglages", @@ -748,18 +733,17 @@ "toolbar.mixed_covered": "« {0} » est placé avant et accepte toutes les extensions cochées ici : ce profil n'est donc jamais utilisé.", "toolbar.mixed_others": "Les autres mélanges de fichiers ouvrent « {0} ».", "toolbar.mixed_others_self": "Il ne reste aucun profil mixte sans case cochée : celui-ci sert donc aussi pour tous les autres mélanges.", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ Même format", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "Formats acceptés : {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} en plus · Shift pour tout afficher", - "toolbar.chip_tip.shift_all": "Shift pour tout afficher", - "toolbar.chip_tip.any_file": "Tout fichier", - "tags.remove": "Retirer", - + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ Même format", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "Formats acceptés : {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} en plus · Shift pour tout afficher", + "toolbar.chip_tip.shift_all": "Shift pour tout afficher", + "toolbar.chip_tip.any_file": "Tout fichier", + "tags.remove": "Retirer", "presets.title": "Préréglages", "presets.add": "Nouveau", "presets.add_empty": "Préréglage vide", @@ -1083,7 +1067,6 @@ "presets.delete_confirm": "Supprimer « {0} » ? Il sera aussi retiré de tous les menus circulaires et groupes.", "presets.name_required": "Saisissez un nom", "presets.select": "Sélectionnez un préréglage pour le modifier", - "theme.title": "Thème", "theme.style": "Style de thème", "theme.style.Soft": "Filee Soft", @@ -1120,7 +1103,6 @@ "theme.preview": "Aperçu", "theme.preview_toggle": "Interrupteur d'exemple (aperçu uniquement, pas un réglage)", "theme.preview_toggle_hint": "Montre l'apparence des interrupteurs avec ce thème. Ne modifie aucun réglage.", - "engines.title": "Moteurs de conversion", "engines.description": "Filee choisit le meilleur moteur disponible pour chaque conversion.\nQuand plusieurs peuvent faire le même travail, le plus haut l'emporte.", "engines.available": "Prêt", @@ -1197,7 +1179,6 @@ "engines.state.unpacking": "Installation…", "engines.state.installed": "Installé", "engines.state.failed": "Échec : {0}", - "about.title": "À propos", "about.updates": "Mises à jour", "common.search": "Rechercher", @@ -1222,7 +1203,6 @@ "feedback.notice_text": "Une étoile sur GitHub aide d'autres personnes à découvrir Filee.\nLes problèmes et les idées sont aussi les bienvenus.", "feedback.star": "Étoile sur GitHub", "feedback.issue": "Signaler un problème ou une idée", - "common.ok": "OK", "common.cancel": "Annuler", "common.save": "Enregistrer", @@ -1232,7 +1212,6 @@ "common.no": "Non", "common.on": "Activé", "common.off": "Désactivé", - "setup.title": "Choisir les moteurs de conversion", "setup.description": "Images, PDF, documents Office, HWP, livres numériques, archives, polices, CAO et modèles 3D se convertissent déjà.\nNe téléchargez des moteurs que pour la vidéo, l'audio ou les formats rares.", "setup.note": "Vous pouvez installer ou supprimer des moteurs à tout moment dans Paramètres → Moteurs.", @@ -1395,7 +1374,6 @@ "watch.status_starting": "Démarrage…", "watch.status_error": "Impossible de surveiller ce dossier : {0}", "watch.cli_hint": "Aussi en ligne de commande : filee watch --to ", - "preset.sheet_studio": "PDF + Modifier", "sheet_studio.title": "Atelier d’impression des feuilles de calcul", "sheet_studio.sheets": "Feuilles et ordre de sortie", @@ -1435,7 +1413,6 @@ "sheet_studio.apply_fix": "Appliquer la mise en page suggérée", "sheet_studio.add_region": "+ Ajouter une autre zone", "sheet_studio.region_name": "{0} · zone {1}", - "whats_new.title": "Nouveautés de Filee {0}", "whats_new.previous": "Précédent", "whats_new.next": "Suivant", @@ -1513,6 +1490,28 @@ "engines.ollama.complete": "Terminé !", "engines.ollama.failed": "Échec :", "engines.ollama.error": "Erreur :", + "preset.upscale": "Mettre à l'échelle l'image", + "preset.upscale.donut": "Mettre à l'échelle", + "engine.upscaler.name": "Modèles de qualité d'image", + "engine.upscaler.description": "Amélioration et super-résolution d'images par IA jusqu'à 4× avec des modèles NCNN locaux.", + "engines.upscaler.modal_description": "Téléchargez des modèles locaux pour agrandir vos images jusqu'à 4×. Tout s'exécute à 100 % hors ligne sur votre appareil.", + "upscale.model": "Modèle", + "upscale.scale": "Facteur d'échelle", + "upscale.output_format": "Format de sortie", + "upscale.format_same": "Même format que la source ({0})", + "upscale.resolution_label": "Aperçu de la résolution", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "Agrandir maintenant", + "upscale.success": "Mise à l'échelle terminée avec succès.", + "error.upscale_failed": "Échec de la mise à l'échelle : {0}", + "engines.package.upscayl-bin.name": "Upscayl (mise à l'échelle d'images par IA)", + "engines.package.upscayl-bin.description": "Mise à l'échelle et super-résolution d'images par IA jusqu'à 4× avec des modèles NCNN locaux.\nLes modèles sont téléchargés à leur première utilisation.", + "error.upscale_needs_engine": "Installez Upscayl dans Paramètres → Moteurs pour utiliser la mise à l'échelle par IA.", + "error.upscale_needs_vulkan": "La mise à l'échelle nécessite une carte graphique (GPU) compatible Vulkan.", + "upscale.model_ready": "Prêt", + "upscale.model_download_size": "Téléchargement de {0} Mo", + "upscale.downloading_model": "Téléchargement du modèle...", + "engines.model.delete_confirm": "Supprimer le modèle {0} ? Vous pouvez le retélécharger à tout moment.", "home.queue_error": "Impossible de convertir {0}. La liste conserve les fichiers.", "home.queue_selected": "sélectionné", "home.queue_remove_file": "Retirer {0}", diff --git a/src/Filee.App/Assets/i18n/hi.json b/src/Filee.App/Assets/i18n/hi.json index 5fe3b77b..0cc35e59 100644 --- a/src/Filee.App/Assets/i18n/hi.json +++ b/src/Filee.App/Assets/i18n/hi.json @@ -17,7 +17,6 @@ "fields.name.video-pro": "वीडियो निर्माण और प्रसारण", "fields.name.audio-pro": "पेशेवर ऑडियो", "fields.name.legacy": "पुराने फ़ॉर्मैट", - "nav.home": "होम", "nav.general": "सामान्य", "nav.triggers": "शॉर्टकट", @@ -734,17 +733,17 @@ "toolbar.mixed_covered": "“{0}” पहले आती है और यहाँ चुने गए हर एक्सटेंशन को अनुमति देती है, इसलिए यह प्रोफ़ाइल कभी इस्तेमाल नहीं होगी।", "toolbar.mixed_others": "फ़ाइलों के अन्य मिश्रण “{0}” खोलते हैं।", "toolbar.mixed_others_self": "कोई मिश्रित प्रोफ़ाइल बिना चयन के नहीं बची है, इसलिए यह प्रोफ़ाइल हर अन्य मिश्रण के लिए भी इस्तेमाल होती है।", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ वही फ़ॉर्मैट", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "स्वीकार्य फ़ॉर्मैट: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} अतिरिक्त · Shift से सभी दिखाएँ", - "toolbar.chip_tip.shift_all": "Shift से सभी दिखाएँ", - "toolbar.chip_tip.any_file": "कोई भी फ़ाइल", - "tags.remove": "हटाएँ", + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ वही फ़ॉर्मैट", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "स्वीकार्य फ़ॉर्मैट: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} अतिरिक्त · Shift से सभी दिखाएँ", + "toolbar.chip_tip.shift_all": "Shift से सभी दिखाएँ", + "toolbar.chip_tip.any_file": "कोई भी फ़ाइल", + "tags.remove": "हटाएँ", "presets.title": "प्रीसेट", "presets.add": "नया", "presets.add_empty": "खाली प्रीसेट", @@ -1375,7 +1374,6 @@ "watch.status_starting": "शुरू हो रहा है…", "watch.status_error": "इस फ़ोल्डर को नहीं देखा जा सकता: {0}", "watch.cli_hint": "कमांड लाइन पर भी: filee watch --to ", - "preset.sheet_studio": "PDF + संपादित करें", "sheet_studio.title": "स्प्रेडशीट प्रिंट स्टूडियो", "sheet_studio.sheets": "शीट और आउटपुट क्रम", @@ -1415,7 +1413,6 @@ "sheet_studio.apply_fix": "सुझाया गया लेआउट लागू करें", "sheet_studio.add_region": "+ दूसरा क्षेत्र जोड़ें", "sheet_studio.region_name": "{0} · क्षेत्र {1}", - "whats_new.title": "Filee {0} में नया क्या है", "whats_new.previous": "पिछला", "whats_new.next": "अगला", @@ -1493,6 +1490,28 @@ "engines.ollama.complete": "पूर्ण हुआ!", "engines.ollama.failed": "विफल:", "engines.ollama.error": "त्रुटि:", + "preset.upscale": "छवि की गुणवत्ता बढ़ाएं", + "preset.upscale.donut": "अपस्केल", + "engine.upscaler.name": "इमेज क्वालिटी मॉडल", + "engine.upscaler.description": "स्थानीय NCNN मॉडल के साथ 4x तक AI छवि सुपर-रिज़ॉल्यूशन और अपस्केलिंग।", + "engines.upscaler.modal_description": "छवियों को 4x तक बड़ा और स्पष्ट करने के लिए स्थानीय AI मॉडल डाउनलोड करें। सभी प्रोसेसिंग आपके डिवाइस पर 100% ऑफ़लाइन होती है।", + "upscale.model": "मॉडल", + "upscale.scale": "स्केल फैक्टर", + "upscale.output_format": "आउटपुट प्रारूप", + "upscale.format_same": "स्रोत प्रारूप जैसा ही ({0})", + "upscale.resolution_label": "रिज़ॉल्यूशन पूर्वावलोकन", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "अभी अपस्केल करें", + "upscale.success": "सफलतापूर्वक अपस्केल किया गया।", + "error.upscale_failed": "अपस्केल विफल रहा: {0}", + "engines.package.upscayl-bin.name": "Upscayl (AI इमेज अपस्केलर)", + "engines.package.upscayl-bin.description": "स्थानीय NCNN मॉडल्स के साथ 4× तक AI इमेज अपस्केलिंग और सुपर-रेज़ोल्यूशन।\nमॉडल पहली बार इस्तेमाल पर डाउनलोड होते हैं।", + "error.upscale_needs_engine": "AI अपस्केलिंग का उपयोग करने के लिए सेटिंग्स → इंजन में Upscayl इंस्टॉल करें।", + "error.upscale_needs_vulkan": "अपस्केलिंग के लिए Vulkan-संगत ग्राफ़िक्स कार्ड (GPU) की आवश्यकता होती है।", + "upscale.model_ready": "तैयार", + "upscale.model_download_size": "{0} MB डाउनलोड", + "upscale.downloading_model": "मॉडल डाउनलोड हो रहा है...", + "engines.model.delete_confirm": "क्या मॉडल {0} को हटाएं? आप इसे किसी भी समय फिर से डाउनलोड कर सकते हैं।", "home.queue_error": "{0} को बदला नहीं जा सका। सूची में फ़ाइलें रहेंगी।", "home.queue_selected": "चुना गया", "home.queue_remove_file": "{0} हटाएं", diff --git a/src/Filee.App/Assets/i18n/id.json b/src/Filee.App/Assets/i18n/id.json index e025be43..52f8a971 100644 --- a/src/Filee.App/Assets/i18n/id.json +++ b/src/Filee.App/Assets/i18n/id.json @@ -17,7 +17,6 @@ "fields.name.video-pro": "Produksi video dan penyiaran", "fields.name.audio-pro": "Audio profesional", "fields.name.legacy": "Format lama", - "nav.home": "Beranda", "nav.general": "Umum", "nav.triggers": "Pintasan", @@ -1491,6 +1490,28 @@ "engines.ollama.complete": "Selesai!", "engines.ollama.failed": "Gagal:", "engines.ollama.error": "Kesalahan:", + "preset.upscale": "Tingkatkan Kualitas Gambar", + "preset.upscale.donut": "Tingkatkan", + "engine.upscaler.name": "Model Kualitas Gambar", + "engine.upscaler.description": "Peningkatan resolusi gambar AI hingga 4× dengan model NCNN lokal.", + "engines.upscaler.modal_description": "Unduh model super-resolusi AI lokal untuk memperbesar gambar hingga 4×. Semua pemrosesan berjalan 100% offline.", + "upscale.model": "Model", + "upscale.scale": "Faktor Skala", + "upscale.output_format": "Format Keluaran", + "upscale.format_same": "Format sama dengan sumber ({0})", + "upscale.resolution_label": "Pratinjau Resolusi", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "Tingkatkan Sekarang", + "upscale.success": "Peningkatan kualitas selesai.", + "error.upscale_failed": "Peningkatan gagal: {0}", + "engines.package.upscayl-bin.name": "Upscayl (peningkat resolusi gambar AI)", + "engines.package.upscayl-bin.description": "Peningkatan resolusi gambar AI hingga 4× dengan model NCNN lokal.\nModel diunduh saat pertama kali digunakan.", + "error.upscale_needs_engine": "Pasang Upscayl di Pengaturan → Engine untuk menggunakan peningkatan resolusi AI.", + "error.upscale_needs_vulkan": "Peningkatan resolusi memerlukan kartu grafis (GPU) yang kompatibel dengan Vulkan.", + "upscale.model_ready": "Siap", + "upscale.model_download_size": "Unduhan {0} MB", + "upscale.downloading_model": "Mengunduh model...", + "engines.model.delete_confirm": "Hapus model {0}? Anda dapat mengunduhnya lagi kapan saja.", "home.queue_error": "Tidak bisa mengonversi {0}. Daftar tetap menyimpan file.", "home.queue_selected": "dipilih", "home.queue_remove_file": "Hapus {0}", diff --git a/src/Filee.App/Assets/i18n/it.json b/src/Filee.App/Assets/i18n/it.json index c3b0704c..72bfab20 100644 --- a/src/Filee.App/Assets/i18n/it.json +++ b/src/Filee.App/Assets/i18n/it.json @@ -1490,6 +1490,28 @@ "engines.ollama.complete": "Completato!", "engines.ollama.failed": "Non riuscito:", "engines.ollama.error": "Errore:", +"preset.upscale": "Migliora e ingrandisci immagine", +"preset.upscale.donut": "Migliora", +"engine.upscaler.name": "Modelli di qualità immagine", +"engine.upscaler.description": "Miglioramento e super-risoluzione immagini con IA fino a 4× con modelli NCNN locali.", +"engines.upscaler.modal_description": "Scarica modelli IA locali per ingrandire le immagini fino a 4×. Tutte le elaborazioni avvengono al 100% offline sul tuo dispositivo.", +"upscale.model": "Modello", +"upscale.scale": "Fattore di scala", +"upscale.output_format": "Formato di output", +"upscale.format_same": "Stesso formato dell'origine ({0})", +"upscale.resolution_label": "Anteprima risoluzione", +"upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", +"upscale.button_start": "Migliora adesso", +"upscale.success": "Miglioramento completato con successo.", +"error.upscale_failed": "Miglioramento non riuscito: {0}", +"engines.package.upscayl-bin.name": "Upscayl (upscaling immagini IA)", +"engines.package.upscayl-bin.description": "Upscaling e super-risoluzione immagini tramite IA fino a 4× con modelli NCNN locali.\nI modelli vengono scaricati la prima volta che li usi.", +"error.upscale_needs_engine": "Installa Upscayl in Impostazioni → Motori per usare l'upscaling IA.", +"error.upscale_needs_vulkan": "L'upscaling richiede una scheda grafica (GPU) compatibile con Vulkan.", +"upscale.model_ready": "Pronto", +"upscale.model_download_size": "Download di {0} MB", +"upscale.downloading_model": "Download del modello in corso...", +"engines.model.delete_confirm": "Eliminare il modello {0}? Puoi scaricarlo nuovamente in qualsiasi momento.", "home.queue_error": "Impossibile convertire {0}. L'elenco conserva i file.", "home.queue_selected": "selezionato", "home.queue_remove_file": "Rimuovi {0}", diff --git a/src/Filee.App/Assets/i18n/ja.json b/src/Filee.App/Assets/i18n/ja.json index 8e5a64c0..55b8af41 100644 --- a/src/Filee.App/Assets/i18n/ja.json +++ b/src/Filee.App/Assets/i18n/ja.json @@ -17,7 +17,6 @@ "fields.name.video-pro": "映像制作・放送", "fields.name.audio-pro": "プロオーディオ", "fields.name.legacy": "古い形式", - "nav.home": "ホーム", "nav.general": "一般", "nav.triggers": "ショートカット", @@ -734,17 +733,17 @@ "toolbar.mixed_covered": "「{0}」の方が一覧で先にあり、ここで選択したすべての拡張子を含むため、このプロファイルは使われません。", "toolbar.mixed_others": "ほかの形式の組み合わせには「{0}」が使われます。", "toolbar.mixed_others_self": "何も選択されていない複数形式用プロファイルがほかにないため、このプロファイルが残りすべての組み合わせにも使われます。", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ 同じ形式", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "対応形式: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": "、", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} 件 · Shift ですべて表示", - "toolbar.chip_tip.shift_all": "Shift ですべて表示", - "toolbar.chip_tip.any_file": "すべてのファイル", - "tags.remove": "削除", + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ 同じ形式", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "対応形式: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": "、", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} 件 · Shift ですべて表示", + "toolbar.chip_tip.shift_all": "Shift ですべて表示", + "toolbar.chip_tip.any_file": "すべてのファイル", + "tags.remove": "削除", "presets.title": "プリセット", "presets.add": "新規", "presets.add_empty": "空のプリセット", @@ -1375,7 +1374,6 @@ "watch.status_starting": "開始中…", "watch.status_error": "このフォルダーを監視できません:{0}", "watch.cli_hint": "コマンドラインからも使えます:filee watch --to ", - "preset.sheet_studio": "PDF + 編集", "sheet_studio.title": "スプレッドシート印刷スタジオ", "sheet_studio.sheets": "シートと出力順序", @@ -1415,7 +1413,6 @@ "sheet_studio.apply_fix": "推奨レイアウトを適用", "sheet_studio.add_region": "+ 別の範囲を追加", "sheet_studio.region_name": "{0} · 範囲 {1}", - "whats_new.title": "Filee {0} の新機能", "whats_new.previous": "前へ", "whats_new.next": "次へ", @@ -1493,6 +1490,28 @@ "engines.ollama.complete": "完了!", "engines.ollama.failed": "失敗:", "engines.ollama.error": "エラー:", + "preset.upscale": "画像をアップスケール", + "preset.upscale.donut": "拡大・高画質化", + "engine.upscaler.name": "画質向上AIモデル", + "engine.upscaler.description": "ローカルNCNNモデルを使用した最大4倍のAI画像超解像・高品質拡大。", + "engines.upscaler.modal_description": "画像を最大4倍に拡大・高画質化するローカルAI超解像モデルをダウンロードします。すべて端末内で100%オフライン処理されます。", + "upscale.model": "モデル", + "upscale.scale": "倍率", + "upscale.output_format": "出力形式", + "upscale.format_same": "元の形式と同じ ({0})", + "upscale.resolution_label": "解像度プレビュー", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "今すぐアップスケール", + "upscale.success": "アップスケールが正常に完了しました。", + "error.upscale_failed": "アップスケールに失敗しました: {0}", + "engines.package.upscayl-bin.name": "Upscayl(AI 画像アップスケーラー)", + "engines.package.upscayl-bin.description": "ローカル NCNN モデルで最大 4 倍の AI 画像アップスケールと超解像処理を行います。\nモデルは初めて使うときにダウンロードされます。", + "error.upscale_needs_engine": "AI アップスケールを使用するには、設定 → エンジンで Upscayl をインストールしてください。", + "error.upscale_needs_vulkan": "アップスケールにはVulkan対応のグラフィックカード(GPU)が必要です。", + "upscale.model_ready": "準備完了", + "upscale.model_download_size": "{0} MB ダウンロード", + "upscale.downloading_model": "モデルをダウンロード中...", + "engines.model.delete_confirm": "モデル {0} を削除しますか?いつでも再ダウンロードできます。", "home.queue_error": "{0} を変換できませんでした。リストのファイルは残ります。", "home.queue_selected": "選択中", "home.queue_remove_file": "{0} を削除", diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json index dfbaf31a..893946e4 100644 --- a/src/Filee.App/Assets/i18n/ko.json +++ b/src/Filee.App/Assets/i18n/ko.json @@ -1,7 +1,6 @@ { "app.name": "Filee", "app.tagline": "끌어서 놓으면, 변환 끝.", - "fields.title": "분야", "fields.description": "쓰지 않는 분야는 꺼 두세요. 꺼진 분야에만 속한 형식은 도넛, 설정 → 원형 툴바의 “사용 가능한 프리셋”, 설정 → 프리셋 목록, 프리셋의 “변환 형식” 목록에서 빠져요. 그 형식의 파일은 계속 알아보고, 명령줄로는 그 형식으로도 변환할 수 있어요. 놓은 파일에는 그 파일 분야의 형식이 그대로 나와요.", "fields.formats": "형식: {0}", @@ -18,7 +17,6 @@ "fields.name.video-pro": "영상 제작·방송", "fields.name.audio-pro": "전문 오디오", "fields.name.legacy": "옛 형식", - "nav.home": "홈", "nav.general": "일반", "nav.triggers": "단축키", @@ -31,7 +29,6 @@ "nav.about": "정보", "nav.collapse": "사이드바 접기", "nav.expand": "사이드바 펼치기", - "tray.open": "Filee 열기", "tray.pause": "단축키 일시 중지", "tray.resume": "단축키 다시 사용", @@ -43,7 +40,6 @@ "close.tray": "트레이에 유지", "close.quit": "종료", "close.dont_ask": "다시 묻지 않기", - "home.title": "홈", "home.drop_title": "여기에 파일을 놓으세요", "home.drop_select": "더블클릭하여 파일 선택", @@ -72,7 +68,6 @@ "home.undone": "되돌렸어요: 결과는 휴지통에 있어요", "home.undone_original": "되돌렸어요: 결과는 휴지통에 있고, 결과가 대신했던 원본도 휴지통에 있어요", "home.undo_failed": "{0}을(를) 휴지통으로 옮기지 못했어요: {1}", - "donut.center_drag": "파일을 이쪽으로", "donut.files_same": "{1} {0}개", "donut.files_mixed": "파일 {0}개", @@ -88,7 +83,6 @@ "donut.group_caption": "묶음 · {0}개", "donut.flip_to_back": "{0} 뒷면", "donut.flip_to_front": "{0} 앞면", - "toast.converting": "{0} → {1}", "toast.done": "완료", "toast.done_errors": "{1}개 중 {0}개 실패", @@ -101,7 +95,6 @@ "toast.get_fonts": "글꼴 받기", "download.badge": "{0}/s · {1}", "download.badge_files": "{0} · {2}개 중 {1}번째", - "error.unsupported_source": "지원하지 않는 파일 형식이에요", "error.empty_file": "빈 파일이에요(0바이트). 우클릭 메뉴의 “새로 만들기”로 만든 파일은 앱에서 열어 저장해야 내용이 생겨요", "error.p7m_unreadable": "Filee가 열 수 있는 서명 파일(.p7m)이 아니거나 손상됐어요", @@ -194,7 +187,6 @@ "notice.geo_field_names": "Shapefile 필드 이름은 10자까지라서 일부를 줄였어요", "notice.geo_shapefile_split": "Shapefile은 한 종류의 도형만 담아서 점·선·면을 파일로 나눠 저장했어요", "notice.geo_heights_dropped": "일부 점에 높이가 없는데 Shapefile은 점마다 비워 둘 수 없어서 높이를 모두 뺐어요", - "engine.reason.not_installed": "설치되지 않음", "engine.reason.error": "확인 중 오류", "engine.reason.unsupported_platform": "이 플랫폼에서는 사용할 수 없음", @@ -257,7 +249,6 @@ "engine.calibre.description": "드문 전자책 형식(LIT, LRF, CHM, PDB, RB…)과 킨들용 출력(MOBI, AZW3).\nHWPX writer보다 위에 두면 전자책 PDF도 만들어요(긴 책은 훨씬 빨라요).", "engine.libreoffice.description": "DOC, PPT, Pages, Keynote, Numbers, 워드퍼펙트, ClarisWorks, Publisher, Visio 같은 오래되었거나 드문 형식을 읽어요.\nDOC·XLS·PPT·ODT·ODP, 오픈도큐먼트 서식 파일, EMF로 저장도 해요.", "engine.translator.description": "오프라인 Ollama 모델을 사용해 문서와 전자책을 컴퓨터에서 번역해요.\nSRT·WebVTT 자막도 번역해요. 모델에는 대사 텍스트만 보내고, 자막 번호와 시간은 그대로 둬요.", - "profile.images": "이미지", "profile.pdf": "PDF", "profile.office": "문서", @@ -277,7 +268,6 @@ "profile.models": "3D 모델", "profile.geo": "지리 정보 (GIS)", "profile.mixed": "혼합 파일", - "preset.crop": "자르기", "crop.save": "자르기 및 저장", "crop.reset": "초기화", @@ -404,7 +394,6 @@ "clip.silence_removed": "무음 구간 제거: {0}", "clip.no_silence": "무음 구간이 없어요", "clip.cannot_play": "클립을 재생할 수 없어요", - "preset.half_size": "50% 축소", "preset.half_size.donut": "50% 축소", "preset.image_edit": "이미지 편집", @@ -519,7 +508,6 @@ "preset.merge_audio": "오디오 합치기", "preset.merge_images": "이미지 합치기", "preset.remove_metadata": "메타데이터 제거", - "format.folder": "폴더", "format.category.Image": "이미지", "format.category.Pdf": "PDF", @@ -539,7 +527,6 @@ "format.category.Data": "데이터", "format.category.Model3D": "3D 모델", "format.category.Geo": "지리 정보", - "general.title": "일반", "general.language": "언어", "general.language_auto": "시스템 기본값", @@ -622,7 +609,6 @@ "general.reset_library": "기본값으로 초기화", "general.reset_confirm": "모든 프리셋과 툴바 프로필을 기본값으로 바꿀까요?", "general.imported": "{0}개 항목을 가져왔어요", - "triggers.title": "단축키", "triggers.description": "원형 툴바를 여는 방법을 정하세요. 제스처는 원하는 만큼 추가할 수 있어요.", "triggers.macos_permissions": "시스템 설정 > 개인정보 보호 및 보안 > 손쉬운 사용에서 Filee를 허용해 주세요.\n이미 켜져 있는데도 제스처가 시작되지 않으면 − 버튼으로 Filee를 지운 뒤 + 버튼으로 다시 추가해 주세요.\n선택 파일 단축키를 처음 쓸 때 Finder 자동화 권한도 요청해요.", @@ -680,7 +666,6 @@ "triggers.excluded_none_running": "창이 열린 다른 앱이 없어요.", "triggers.excluded_note": "마우스가 이 앱 위에 있으면 제스처와 단축키가 동작하지 않아요.", "triggers.paused": "단축키가 일시 중지되었어요", - "toolbar.title": "원형 툴바", "toolbar.step_profile": "1. 프로필 선택", "toolbar.step_arrange": "2. 프리셋 배치", @@ -748,18 +733,17 @@ "toolbar.mixed_covered": "목록에서 먼저인 “{0}”이(가) 여기서 체크한 확장자를 모두 허용해서 이 프로필은 쓰이지 않아요.", "toolbar.mixed_others": "그 밖의 혼합 선택에는 “{0}”이(가) 열려요.", "toolbar.mixed_others_self": "아무것도 체크하지 않은 혼합 프로필이 없어서 그 밖의 혼합 선택에도 이 프로필이 쓰여요.", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ 같은 형식", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "받을 수 있는 형식 {0}개", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} 외 {1}개", - "toolbar.chip_tip.more_shift": "그 밖에 {0}개 형식 · Shift로 전부 보기", - "toolbar.chip_tip.shift_all": "Shift로 전부 보기", - "toolbar.chip_tip.any_file": "모든 파일", - "tags.remove": "빼기", - + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ 같은 형식", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "받을 수 있는 형식 {0}개", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} 외 {1}개", + "toolbar.chip_tip.more_shift": "그 밖에 {0}개 형식 · Shift로 전부 보기", + "toolbar.chip_tip.shift_all": "Shift로 전부 보기", + "toolbar.chip_tip.any_file": "모든 파일", + "tags.remove": "빼기", "presets.title": "프리셋", "presets.add": "새로 만들기", "presets.add_empty": "빈 프리셋", @@ -1083,7 +1067,6 @@ "presets.delete_confirm": "“{0}”을(를) 삭제할까요? 모든 툴바와 묶음에서도 빠져요.", "presets.name_required": "이름을 입력하세요", "presets.select": "편집할 프리셋을 선택하세요", - "theme.title": "테마", "theme.style": "테마 스타일", "theme.style.Soft": "Filee Soft", @@ -1120,7 +1103,6 @@ "theme.preview": "미리보기", "theme.preview_toggle": "예시 스위치(미리보기 전용, 설정 아님)", "theme.preview_toggle_hint": "이 테마에서 스위치가 어떻게 보이는지 보여 줘요. 어떤 설정도 바꾸지 않아요.", - "engines.title": "변환 엔진", "engines.description": "Filee는 변환마다 쓸 수 있는 엔진 중 가장 좋은 것을 골라요.\n여러 엔진이 같은 변환을 할 수 있으면 목록 위쪽이 우선이에요.", "engines.available": "사용 가능", @@ -1197,7 +1179,6 @@ "engines.state.unpacking": "설치하는 중…", "engines.state.installed": "설치됨", "engines.state.failed": "실패: {0}", - "about.title": "정보", "about.updates": "업데이트", "common.search": "검색", @@ -1222,7 +1203,6 @@ "feedback.notice_text": "GitHub 별(⭐)은 다른 분들이 Filee를 찾는 데 큰 도움이 돼요.\n불편한 점이나 원하는 기능도 알려 주세요.", "feedback.star": "GitHub에서 별 주기", "feedback.issue": "문제 신고 · 기능 제안", - "common.ok": "확인", "common.cancel": "취소", "common.save": "저장", @@ -1232,7 +1212,6 @@ "common.no": "아니요", "common.on": "켬", "common.off": "끔", - "setup.title": "변환 엔진 선택", "setup.description": "이미지, PDF, 오피스 문서, HWP, 전자책, 압축 파일, 글꼴, CAD, 3D 모델은 지금 바로 변환돼요.\n동영상·오디오나 드문 형식이 필요할 때만 엔진을 받으세요.", "setup.note": "엔진은 언제든 설정 → 변환 엔진에서 설치하거나 제거할 수 있어요.", @@ -1395,7 +1374,6 @@ "watch.status_starting": "시작하는 중…", "watch.status_error": "이 폴더를 감시할 수 없어요: {0}", "watch.cli_hint": "명령줄에서도 쓸 수 있어요: filee watch <폴더> --to <형식>", - "preset.sheet_studio": "PDF + 편집", "sheet_studio.title": "스프레드시트 인쇄 스튜디오", "sheet_studio.sheets": "시트 및 출력 순서", @@ -1435,7 +1413,6 @@ "sheet_studio.apply_fix": "권장 레이아웃 적용", "sheet_studio.add_region": "+ 다른 영역 추가", "sheet_studio.region_name": "{0} · 영역 {1}", - "whats_new.title": "Filee {0}의 새 기능", "whats_new.previous": "이전", "whats_new.next": "다음", @@ -1513,6 +1490,28 @@ "engines.ollama.complete": "완료!", "engines.ollama.failed": "실패:", "engines.ollama.error": "오류:", + "preset.upscale": "이미지 고화질 확대", + "preset.upscale.donut": "고화질 확대", + "engine.upscaler.name": "화질 향상 모델", + "engine.upscaler.description": "로컬 NCNN 모델을 사용한 최대 4배 AI 이미지 초해상도 및 고화질 확대예요.", + "engines.upscaler.modal_description": "이미지를 최대 4배까지 선명하게 확대하는 로컬 AI 모델을 다운로드해요. 모든 작업은 컴퓨터 내에서 100% 오프라인으로 실행돼요.", + "upscale.model": "모델", + "upscale.scale": "확대 배율", + "upscale.output_format": "출력 형식", + "upscale.format_same": "원본 형식 유지 ({0})", + "upscale.resolution_label": "해상도 미리보기", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}배)", + "upscale.button_start": "지금 확대하기", + "upscale.success": "고화질 확대가 완료되었어요.", + "error.upscale_failed": "확대에 실패했어요: {0}", + "engines.package.upscayl-bin.name": "Upscayl (AI 이미지 업스케일러)", + "engines.package.upscayl-bin.description": "로컬 NCNN 모델로 최대 4배 AI 이미지 업스케일 및 초해상도를 지원해요.\n모델은 처음 쓸 때 내려받아요.", + "error.upscale_needs_engine": "AI 업스케일을 사용하려면 설정 → 엔진에서 Upscayl을 설치해 주세요.", + "error.upscale_needs_vulkan": "업스케일링에는 Vulkan 호환 그래픽 카드(GPU)가 필요합니다.", + "upscale.model_ready": "준비됨", + "upscale.model_download_size": "{0} MB 다운로드", + "upscale.downloading_model": "모델 내려받는 중...", + "engines.model.delete_confirm": "{0} 모델을 삭제할까요? 언제든 다시 내려받을 수 있어요.", "home.queue_error": "{0}을(를) 변환하지 못했어요. 목록의 파일은 그대로 남아 있어요.", "home.queue_selected": "선택됨", "home.queue_remove_file": "{0} 제거", diff --git a/src/Filee.App/Assets/i18n/ne.json b/src/Filee.App/Assets/i18n/ne.json index 961f28be..5ec10521 100644 --- a/src/Filee.App/Assets/i18n/ne.json +++ b/src/Filee.App/Assets/i18n/ne.json @@ -17,7 +17,6 @@ "fields.name.video-pro": "भिडियो उत्पादन र प्रसारण", "fields.name.audio-pro": "व्यावसायिक अडियो", "fields.name.legacy": "पुराना ढाँचाहरू", - "nav.home": "गृहपृष्ठ", "nav.general": "सामान्य", "nav.triggers": "सर्टकटहरू", @@ -734,17 +733,17 @@ "toolbar.mixed_covered": "“{0}” पहिले आउँछ र यहाँ जाँच गरिएका सबै एक्सटेन्सनहरूलाई अनुमति दिन्छ, त्यसैले यो प्रोफाइल कहिल्यै प्रयोग हुँदैन।", "toolbar.mixed_others": "फाइलहरूको अन्य मिश्रणहरूले “{0}” खोल्छन्।", "toolbar.mixed_others_self": "कुनै पनि अनचेक गरिएको मिश्रित प्रोफाइल बाँकी छैन, त्यसैले यो प्रोफाइल हरेक अन्य मिश्रणका लागि पनि प्रयोग हुन्छ।", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ उही ढाँचा", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "समर्थित ढाँचाहरू: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} थप · Shift ले सबै देखाउँछ", - "toolbar.chip_tip.shift_all": "Shift ले सबै देखाउँछ", - "toolbar.chip_tip.any_file": "कुनै पनि फाइल", - "tags.remove": "ट्याग हटाउनुहोस्", + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ उही ढाँचा", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "समर्थित ढाँचाहरू: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} थप · Shift ले सबै देखाउँछ", + "toolbar.chip_tip.shift_all": "Shift ले सबै देखाउँछ", + "toolbar.chip_tip.any_file": "कुनै पनि फाइल", + "tags.remove": "ट्याग हटाउनुहोस्", "presets.title": "प्रिसेटहरू", "presets.add": "नयाँ", "presets.add_empty": "खाली प्रिसेट", @@ -1375,7 +1374,6 @@ "watch.status_starting": "सुरु हुँदैछ…", "watch.status_error": "यो फोल्डर निगरानी गर्न सकिँदैन: {0}", "watch.cli_hint": "कमान्ड लाइनमा पनि: filee watch --to ", - "preset.sheet_studio": "PDF + सम्पादन", "sheet_studio.title": "स्प्रेडसिट प्रिन्ट स्टुडियो", "sheet_studio.sheets": "सिटहरू र आउटपुट क्रम", @@ -1415,7 +1413,6 @@ "sheet_studio.apply_fix": "सुझाइएको लेआउट लागू गर्नुहोस्", "sheet_studio.add_region": "+ अर्को क्षेत्र थप्नुहोस्", "sheet_studio.region_name": "{0} · क्षेत्र {1}", - "whats_new.title": "Filee {0} मा के नयाँ छ", "whats_new.previous": "अघिल्लो", "whats_new.next": "अर्को", @@ -1493,6 +1490,28 @@ "engines.ollama.complete": "सम्पन्न भयो!", "engines.ollama.failed": "विफल भयो:", "engines.ollama.error": "त्रुटि:", + "preset.upscale": "छविको गुणस्तर बढाउनुहोस्", + "preset.upscale.donut": "अपस्केल", + "engine.upscaler.name": "छवि गुणस्तर मोडेलहरू", + "engine.upscaler.description": "स्थानीय NCNN मोडेलहरूसँग ४× सम्म AI छवि सुपर-रिजोल्युसन र अपस्केलिङ।", + "engines.upscaler.modal_description": "तस्बिरहरूलाई ४× सम्म विस्तार गर्न स्थानीय AI सुपर-रिजोल्युसन मोडेलहरू डाउनलोड गर्नुहोस्। सबै प्रक्रिया १००% अफलाइन चल्छ।", + "upscale.model": "मोडेल", + "upscale.scale": "स्केल कारक", + "upscale.output_format": "आउटपुट ढाँचा", + "upscale.format_same": "स्रोत जस्तै ढाँचा ({0})", + "upscale.resolution_label": "रिजोल्युसन पूर्वावलोकन", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "अहिले अपस्केल गर्नुहोस्", + "upscale.success": "अपस्केल सफलतापूर्वक सम्पन्न भयो।", + "error.upscale_failed": "अपस्केल असफल भयो: {0}", + "engines.package.upscayl-bin.name": "Upscayl (AI तस्बिर अपस्केलर)", + "engines.package.upscayl-bin.description": "स्थानीय NCNN मोडेलहरूसँग ४ गुणासम्म AI तस्बिर अपस्केलिङ र सुपर-रिजोल्युसन।\nमोडेलहरू पहिलो पटक प्रयोग गर्दा डाउनलोड हुन्छन्।", + "error.upscale_needs_engine": "AI अपस्केलिङ प्रयोग गर्न सेटिङहरू → इन्जिनहरूमा Upscayl स्थापना गर्नुहोस्।", + "error.upscale_needs_vulkan": "अपस्केलिङका लागि Vulkan-मिल्दो ग्राफिक्स कार्ड (GPU) आवश्यक पर्छ।", + "upscale.model_ready": "तयार", + "upscale.model_download_size": "{0} MB डाउनलोड", + "upscale.downloading_model": "मोडेल डाउनलोड हुँदैछ...", + "engines.model.delete_confirm": "मोडेल {0} मेटाउनुहुन्छ? तपाईं यसलाई कुनै पनि समयमा पुन: डाउनलोड गर्न सक्नुहुन्छ।", "home.queue_error": "{0} रूपान्तरण गर्न सकिएन। सूचीमा फाइलहरू रहन्छन्।", "home.queue_selected": "चयन गरिएको", "home.queue_remove_file": "{0} हटाउनुहोस्", diff --git a/src/Filee.App/Assets/i18n/pl.json b/src/Filee.App/Assets/i18n/pl.json index a59cabbc..25a5c773 100644 --- a/src/Filee.App/Assets/i18n/pl.json +++ b/src/Filee.App/Assets/i18n/pl.json @@ -1,7 +1,6 @@ { "app.name": "Filee", "app.tagline": "Przeciągnij, upuść, gotowe.", - "fields.title": "Dziedziny", "fields.description": "Wyłącz dziedziny, w których nie pracujesz. Formaty należące wyłącznie do wyłączonych dziedzin nie pojawią się w wycinkach pierścienia, w sekcji „Dostępne ustawienia” w Ustawienia → Pasek-pierścień, na liście w Ustawienia → Ustawienia wstępne ani na liście „Konwertuj do” podczas edycji ustawienia. Filee nadal rozpoznaje pliki w tych formatach, a wiersz poleceń nadal może na nie konwertować. Upuszczony plik nadal oferuje formaty swoich dziedzin.", "fields.formats": "Formaty: {0}", @@ -18,7 +17,6 @@ "fields.name.video-pro": "Produkcja wideo i nadawanie", "fields.name.audio-pro": "Profesjonalne audio", "fields.name.legacy": "Stare formaty", - "nav.home": "Start", "nav.general": "Ogólne", "nav.triggers": "Skróty", @@ -31,7 +29,6 @@ "nav.about": "O programie", "nav.collapse": "Zwiń pasek boczny", "nav.expand": "Rozwiń pasek boczny", - "tray.open": "Otwórz Filee", "tray.pause": "Wstrzymaj skróty", "tray.resume": "Wznów skróty", @@ -43,7 +40,6 @@ "close.tray": "Zostaw w zasobniku", "close.quit": "Zakończ", "close.dont_ask": "Nie pytaj ponownie", - "home.title": "Start", "home.drop_title": "Upuść pliki tutaj", "home.drop_select": "Kliknij dwukrotnie, aby wybrać pliki", @@ -72,7 +68,6 @@ "home.undone": "Cofnięto: wyniki są w Koszu", "home.undone_original": "Cofnięto: wyniki są w Koszu, tak jak oryginały, które zastąpiły", "home.undo_failed": "Nie udało się przenieść {0} do Kosza: {1}", - "donut.center_drag": "Przenieś pliki tutaj", "donut.files_same": "{0} × {1}", "donut.files_mixed": "Pliki: {0}", @@ -88,7 +83,6 @@ "donut.group_caption": "Grupa: {0}", "donut.flip_to_back": "{0} tył", "donut.flip_to_front": "{0} przód", - "toast.converting": "{0} → {1}", "toast.done": "Gotowe", "toast.done_errors": "Nieudane: {0} z {1}", @@ -101,7 +95,6 @@ "toast.get_fonts": "Pobierz czcionki", "download.badge": "{0}/s · {1}", "download.badge_files": "{0} · plik {1} z {2}", - "error.unsupported_source": "Ten typ pliku nie jest obsługiwany", "error.empty_file": "Plik jest pusty (0 bajtów). Plik utworzony przez „Nowy” w menu kontekstowym pozostaje pusty, dopóki nie otworzysz go w jego aplikacji i nie zapiszesz", "error.p7m_unreadable": "To nie jest podpisany plik .p7m, który Filee może otworzyć, albo jest uszkodzony", @@ -194,7 +187,6 @@ "notice.geo_field_names": "Nazwy pól Shapefile mają najwyżej 10 znaków: niektóre skrócono", "notice.geo_shapefile_split": "Shapefile przechowuje jeden rodzaj kształtu, więc punkty, linie i poligony zapisano w osobnych plikach", "notice.geo_heights_dropped": "Niektóre punkty nie mają wysokości, a Shapefile nie może pominąć tylko jednej, więc pominięto wszystkie wysokości", - "engine.reason.not_installed": "Nie zainstalowano", "engine.reason.error": "Błąd podczas sprawdzania", "engine.reason.unsupported_platform": "Niedostępne na tej platformie", @@ -257,7 +249,6 @@ "engine.calibre.description": "Rzadkie formaty e-booków (LIT, LRF, CHM, PDB, RB…) i zapis dla Kindle (MOBI, AZW3).\nUmieszczony nad HWPX writer tworzy też PDF-y e-booków (znacznie szybciej przy długich książkach).", "engine.libreoffice.description": "Odczytuje starsze i rzadsze formaty: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nZapisuje też jako DOC, XLS, PPT, ODT, ODP, szablony OpenDocument i EMF.", "engine.translator.description": "Tłumaczy dokumenty i e-booki lokalnie za pomocą modeli offline Ollama.\nTłumaczy też napisy SRT i WebVTT: do modelu trafia tylko tekst napisów, a numery i znaczniki czasu pozostają bez zmian.", - "profile.images": "Obrazy", "profile.pdf": "PDF", "profile.office": "Dokumenty", @@ -277,7 +268,6 @@ "profile.models": "Modele 3D", "profile.geo": "Geoprzestrzenne", "profile.mixed": "Różne pliki", - "preset.crop": "Kadruj", "crop.save": "Kadruj i zapisz", "crop.reset": "Resetuj", @@ -404,7 +394,6 @@ "clip.silence_removed": "Usunięto ciszę: {0}", "clip.no_silence": "Nie znaleziono ciszy", "clip.cannot_play": "Nie można odtworzyć klipu", - "preset.half_size": "Połowa rozmiaru", "preset.half_size.donut": "Połowa rozmiaru", "preset.image_edit": "Edytuj obrazy", @@ -519,7 +508,6 @@ "preset.merge_audio": "Połącz dźwięk", "preset.merge_images": "Połącz obrazy", "preset.remove_metadata": "Usuń metadane", - "format.folder": "Folder", "format.category.Image": "Obraz", "format.category.Pdf": "PDF", @@ -539,7 +527,6 @@ "format.category.Data": "Dane", "format.category.Model3D": "Model 3D", "format.category.Geo": "Geoprzestrzenne", - "general.title": "Ogólne", "general.language": "Język", "general.language_auto": "Domyślny systemu", @@ -622,7 +609,6 @@ "general.reset_library": "Przywróć domyślne", "general.reset_confirm": "Zastąpić wszystkie ustawienia wstępne i profile pasków domyślnymi?", "general.imported": "Zaimportowano elementów: {0}", - "triggers.title": "Skróty", "triggers.description": "Wybierz, jak otwiera się pasek-pierścień. Możesz dodać dowolnie wiele gestów.", "triggers.macos_permissions": "Zezwól Filee w Ustawienia systemowe > Prywatność i ochrona > Dostępność.\nJeśli Filee jest już włączony, a gesty się nie uruchamiają, usuń go przyciskiem − i dodaj ponownie przyciskiem +.\nFinder poprosi też o uprawnienie Automatyzacji przy pierwszym użyciu skrótu zaznaczenia.", @@ -680,7 +666,6 @@ "triggers.excluded_none_running": "Żadna inna aplikacja nie ma otwartego okna.", "triggers.excluded_note": "Gesty i skróty nic nie robią, gdy wskaźnik jest nad tymi aplikacjami.", "triggers.paused": "Skróty są wstrzymane", - "toolbar.title": "Pasek-pierścień", "toolbar.step_profile": "1. Wybierz profil", "toolbar.step_arrange": "2. Ułóż ustawienia", @@ -748,18 +733,17 @@ "toolbar.mixed_covered": "„{0}” jest pierwszy i dopuszcza każde zaznaczone tu rozszerzenie, więc ten profil nigdy nie jest używany.", "toolbar.mixed_others": "Inne mieszanki plików otwierają „{0}”.", "toolbar.mixed_others_self": "Nie został żaden mieszany profil bez zaznaczeń, więc ten jest używany także dla każdej innej mieszanki.", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ Ten sam format", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "Obsługiwane formaty: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} dodatkowo · Shift pokazuje wszystkie", - "toolbar.chip_tip.shift_all": "Shift pokazuje wszystkie", - "toolbar.chip_tip.any_file": "Dowolny plik", - "tags.remove": "Usuń", - + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ Ten sam format", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "Obsługiwane formaty: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} dodatkowo · Shift pokazuje wszystkie", + "toolbar.chip_tip.shift_all": "Shift pokazuje wszystkie", + "toolbar.chip_tip.any_file": "Dowolny plik", + "tags.remove": "Usuń", "presets.title": "Ustawienia wstępne", "presets.add": "Nowe", "presets.add_empty": "Pusty preset", @@ -1083,7 +1067,6 @@ "presets.delete_confirm": "Usunąć „{0}”? Zostanie też usunięte z każdego paska i grupy.", "presets.name_required": "Podaj nazwę", "presets.select": "Wybierz ustawienie, aby je edytować", - "theme.title": "Motyw", "theme.style": "Styl motywu", "theme.style.Soft": "Filee Soft", @@ -1120,7 +1103,6 @@ "theme.preview": "Podgląd", "theme.preview_toggle": "Przykładowy przełącznik (tylko podgląd, nie ustawienie)", "theme.preview_toggle_hint": "Pokazuje, jak wyglądają przełączniki w tym motywie. Nie zmienia żadnego ustawienia.", - "engines.title": "Silniki konwersji", "engines.description": "Filee wybiera najlepszy dostępny silnik dla każdej konwersji.\nGdy kilka potrafi to samo, wygrywa wyższy.", "engines.available": "Gotowy", @@ -1197,7 +1179,6 @@ "engines.state.unpacking": "Instalowanie…", "engines.state.installed": "Zainstalowano", "engines.state.failed": "Błąd: {0}", - "about.title": "O programie", "about.updates": "Aktualizacje", "common.search": "Szukaj", @@ -1222,7 +1203,6 @@ "feedback.notice_text": "Gwiazdka na GitHubie pomaga innym znaleźć Filee.\nProblemy i pomysły też są mile widziane.", "feedback.star": "Gwiazdka na GitHubie", "feedback.issue": "Zgłoś problem lub pomysł", - "common.ok": "OK", "common.cancel": "Anuluj", "common.save": "Zapisz", @@ -1232,7 +1212,6 @@ "common.no": "Nie", "common.on": "Wł.", "common.off": "Wył.", - "setup.title": "Wybierz silniki konwersji", "setup.description": "Obrazy, PDF, dokumenty Office, HWP, e-booki, archiwa, czcionki, CAD i modele 3D już się konwertują.\nPobieraj silniki tylko dla wideo i audio lub rzadkich formatów.", "setup.note": "Silniki możesz w każdej chwili zainstalować lub usunąć w Ustawienia → Silniki.", @@ -1395,7 +1374,6 @@ "watch.status_starting": "Uruchamianie…", "watch.status_error": "Nie można obserwować tego folderu: {0}", "watch.cli_hint": "Także w wierszu poleceń: filee watch --to ", - "preset.sheet_studio": "PDF + Edycja", "sheet_studio.title": "Studio wydruku arkuszy", "sheet_studio.sheets": "Arkusze i kolejność wyjściowa", @@ -1435,7 +1413,6 @@ "sheet_studio.apply_fix": "Zastosuj sugerowany układ", "sheet_studio.add_region": "+ Dodaj kolejny obszar", "sheet_studio.region_name": "{0} · obszar {1}", - "whats_new.title": "Nowości w Filee {0}", "whats_new.previous": "Wstecz", "whats_new.next": "Dalej", @@ -1513,6 +1490,28 @@ "engines.ollama.complete": "Ukończono!", "engines.ollama.failed": "Niepowodzenie:", "engines.ollama.error": "Błąd:", + "preset.upscale": "Zwiększ rozdzielczość obrazu", + "preset.upscale.donut": "Zwiększ jakość", + "engine.upscaler.name": "Modele jakości obrazu", + "engine.upscaler.description": "Skalowanie AI i super-rozdzielczość obrazów do 4× za pomocą lokalnych modeli NCNN.", + "engines.upscaler.modal_description": "Pobierz lokalne modele super-rozdzielczości, aby powiększać obrazy do 4×. Wszystko działa w 100% offline na Twoim komputerze.", + "upscale.model": "Model", + "upscale.scale": "Mnożnik skali", + "upscale.output_format": "Format wyjściowy", + "upscale.format_same": "Taki sam jak źródłowy ({0})", + "upscale.resolution_label": "Podgląd rozdzielczości", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "Zwiększ jakość teraz", + "upscale.success": "Skalowanie zakończone pomyślnie.", + "error.upscale_failed": "Skalowanie nie powiodło się: {0}", + "engines.package.upscayl-bin.name": "Upscayl (skalowanie obrazu SI)", + "engines.package.upscayl-bin.description": "Skalowanie obrazu SI i super-rozdzielczość do 4× za pomocą lokalnych modeli NCNN.\nModele są pobierane przy pierwszym użyciu.", + "error.upscale_needs_engine": "Zainstaluj Upscayl w Ustawienia → Silniki, aby korzystać ze skalowania SI.", + "error.upscale_needs_vulkan": "Zwiększanie rozdzielczości wymaga karty graficznej (GPU) zgodnej z Vulkan.", + "upscale.model_ready": "Gotowy", + "upscale.model_download_size": "Pobieranie {0} MB", + "upscale.downloading_model": "Pobieranie modelu...", + "engines.model.delete_confirm": "Usunąć model {0}? Możesz pobrać go ponownie w dowolnym momencie.", "home.queue_error": "Nie udało się przekonwertować: {0}. Lista zachowuje pliki.", "home.queue_selected": "zaznaczony", "home.queue_remove_file": "Usuń {0}", diff --git a/src/Filee.App/Assets/i18n/pt-BR.json b/src/Filee.App/Assets/i18n/pt-BR.json index 742fe9c3..acafe999 100644 --- a/src/Filee.App/Assets/i18n/pt-BR.json +++ b/src/Filee.App/Assets/i18n/pt-BR.json @@ -1,7 +1,6 @@ { "app.name": "Filee", "app.tagline": "Arraste, solte, convertido.", - "fields.title": "Áreas", "fields.description": "Desative as áreas com as quais você não trabalha. Formatos que pertencem apenas a áreas desativadas não aparecem nas fatias do menu circular, em “Predefinições disponíveis” de Configurações → Menu circular, na lista de Configurações → Predefinições nem na lista “Converter para” de uma predefinição. O Filee continua reconhecendo esses arquivos e a linha de comando ainda converte para esses formatos. Um arquivo solto continua oferecendo os formatos de suas próprias áreas.", "fields.formats": "Formatos: {0}", @@ -18,7 +17,6 @@ "fields.name.video-pro": "Produção de vídeo e transmissão", "fields.name.audio-pro": "Áudio profissional", "fields.name.legacy": "Formatos antigos", - "nav.home": "Início", "nav.general": "Geral", "nav.triggers": "Atalhos", @@ -31,7 +29,6 @@ "nav.about": "Sobre", "nav.collapse": "Recolher barra lateral", "nav.expand": "Expandir barra lateral", - "tray.open": "Abrir o Filee", "tray.pause": "Pausar atalhos", "tray.resume": "Retomar atalhos", @@ -43,7 +40,6 @@ "close.tray": "Manter na bandeja", "close.quit": "Sair", "close.dont_ask": "Não perguntar de novo", - "home.title": "Início", "home.drop_title": "Solte os arquivos aqui", "home.drop_select": "Clique duas vezes para selecionar arquivos", @@ -72,7 +68,6 @@ "home.undone": "Desfeito: os resultados estão na Lixeira", "home.undone_original": "Desfeito: os resultados estão na Lixeira, assim como os originais que eles substituíram", "home.undo_failed": "Não foi possível mover {0} para a Lixeira: {1}", - "donut.center_drag": "Traga os arquivos para cá", "donut.files_same": "{0} × {1}", "donut.files_mixed": "{0} arquivos", @@ -88,7 +83,6 @@ "donut.group_caption": "Grupo de {0}", "donut.flip_to_back": "{0} verso", "donut.flip_to_front": "{0} frente", - "toast.converting": "{0} → {1}", "toast.done": "Concluído", "toast.done_errors": "{0} de {1} falharam", @@ -101,7 +95,6 @@ "toast.get_fonts": "Obter fontes", "download.badge": "{0}/s · {1}", "download.badge_files": "{0} · {1} de {2} arquivos", - "error.unsupported_source": "Este tipo de arquivo não é compatível", "error.empty_file": "O arquivo está vazio (0 bytes). Um arquivo criado com “Novo” no menu de contexto continua vazio até você abri-lo no aplicativo e salvá-lo", "error.p7m_unreadable": "Este não é um arquivo assinado .p7m que o Filee consiga abrir, ou está danificado", @@ -194,7 +187,6 @@ "notice.geo_field_names": "Nomes de campo de Shapefile têm no máximo 10 caracteres: alguns foram encurtados", "notice.geo_shapefile_split": "Um Shapefile guarda um tipo de forma, então pontos, linhas e polígonos foram salvos em arquivos separados", "notice.geo_heights_dropped": "Alguns pontos não têm altura e o Shapefile não pode deixá-la de fora em apenas um, então todas as alturas foram omitidas", - "engine.reason.not_installed": "Não instalado", "engine.reason.error": "Erro ao verificar", "engine.reason.unsupported_platform": "Indisponível nesta plataforma", @@ -257,7 +249,6 @@ "engine.calibre.description": "Formatos raros de e-book (LIT, LRF, CHM, PDB, RB…) e saída Kindle (MOBI, AZW3).\nAcima do gravador de HWPX, também gera os PDFs de e-books (bem mais rápido em livros longos).", "engine.libreoffice.description": "Lê formatos antigos e menos comuns: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nTambém salva como DOC, XLS, PPT, ODT, ODP, modelos OpenDocument e EMF.", "engine.translator.description": "Traduz documentos e e-books localmente usando modelos offline do Ollama.\nTambém traduz legendas SRT e WebVTT: só o texto de cada legenda é enviado ao modelo; os números e os tempos das legendas são mantidos.", - "profile.images": "Imagens", "profile.pdf": "PDF", "profile.office": "Documentos", @@ -277,7 +268,6 @@ "profile.models": "Modelos 3D", "profile.geo": "Geoespacial", "profile.mixed": "Arquivos mistos", - "preset.crop": "Recortar", "preset.clip": "Cortar um clipe", "preset.group_crop_video": "Recortar vídeo", @@ -404,7 +394,6 @@ "clip.silence_removed": "Silêncio removido: {0}", "clip.no_silence": "Nenhum silêncio encontrado", "clip.cannot_play": "Não foi possível reproduzir o clipe", - "preset.half_size": "Reduzir 50%", "preset.half_size.donut": "Tamanho 50%", "preset.image_edit": "Editar imagens", @@ -519,7 +508,6 @@ "preset.merge_audio": "Juntar áudio", "preset.merge_images": "Juntar imagens", "preset.remove_metadata": "Remover metadados", - "format.folder": "Pasta", "format.category.Image": "Imagem", "format.category.Pdf": "PDF", @@ -539,7 +527,6 @@ "format.category.Data": "Dados", "format.category.Model3D": "Modelo 3D", "format.category.Geo": "Geoespacial", - "general.title": "Geral", "general.language": "Idioma", "general.language_auto": "Padrão do sistema", @@ -622,7 +609,6 @@ "general.reset_library": "Restaurar padrões", "general.reset_confirm": "Substituir todas as predefinições e os perfis de menu pelos padrões?", "general.imported": "{0} itens importados", - "triggers.title": "Atalhos", "triggers.description": "Escolha como o menu circular abre. Você pode adicionar quantos gestos quiser.", "triggers.macos_permissions": "Permita o Filee em Ajustes do Sistema > Privacidade e Segurança > Acessibilidade.\nSe o Filee já estiver ativado, mas os gestos não iniciarem, remova-o com − e adicione-o de novo com +.\nO Finder também pede permissão de Automação na primeira vez que você usa um atalho de seleção.", @@ -680,7 +666,6 @@ "triggers.excluded_none_running": "Nenhum outro app tem uma janela aberta.", "triggers.excluded_note": "Gestos e atalhos não fazem nada enquanto o ponteiro está sobre estes apps.", "triggers.paused": "Os atalhos estão pausados", - "toolbar.title": "Menu circular", "toolbar.step_profile": "1. Escolha um perfil", "toolbar.step_arrange": "2. Organize as predefinições", @@ -748,18 +733,17 @@ "toolbar.mixed_covered": "“{0}” vem primeiro e permite toda extensão marcada aqui, então este perfil nunca é usado.", "toolbar.mixed_others": "Outras misturas de arquivos abrem “{0}”.", "toolbar.mixed_others_self": "Não restou perfil misto sem nada marcado, então este também é usado para toda outra mistura.", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ Mesmo formato", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "Formatos aceitos: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} a mais · Shift para mostrar todos", - "toolbar.chip_tip.shift_all": "Shift para mostrar todos", - "toolbar.chip_tip.any_file": "Qualquer arquivo", - "tags.remove": "Remover", - + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ Mesmo formato", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "Formatos aceitos: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} a mais · Shift para mostrar todos", + "toolbar.chip_tip.shift_all": "Shift para mostrar todos", + "toolbar.chip_tip.any_file": "Qualquer arquivo", + "tags.remove": "Remover", "presets.title": "Predefinições", "presets.add": "Nova", "presets.add_empty": "Predefinição vazia", @@ -1083,7 +1067,6 @@ "presets.delete_confirm": "Excluir “{0}”? Também será removida de todo menu e grupo.", "presets.name_required": "Digite um nome", "presets.select": "Selecione uma predefinição para editá-la", - "theme.title": "Tema", "theme.style": "Estilo do tema", "theme.style.Soft": "Filee Soft", @@ -1120,7 +1103,6 @@ "theme.preview": "Prévia", "theme.preview_toggle": "Chave de exemplo (só pré-visualização, não é uma configuração)", "theme.preview_toggle_hint": "Mostra como as chaves ficam neste tema. Não altera nenhuma configuração.", - "engines.title": "Motores de conversão", "engines.description": "O Filee escolhe o melhor motor disponível para cada conversão.\nQuando vários fazem o mesmo trabalho, o que está mais acima tem prioridade.", "engines.available": "Pronto", @@ -1197,7 +1179,6 @@ "engines.state.unpacking": "Instalando…", "engines.state.installed": "Instalado", "engines.state.failed": "Falhou: {0}", - "about.title": "Sobre", "about.updates": "Atualizações", "common.search": "Pesquisar", @@ -1222,7 +1203,6 @@ "feedback.notice_text": "Uma estrela no GitHub ajuda outras pessoas a encontrar o Filee.\nProblemas e ideias também são bem-vindos.", "feedback.star": "Dar estrela no GitHub", "feedback.issue": "Relatar um problema ou uma ideia", - "common.ok": "OK", "common.cancel": "Cancelar", "common.save": "Salvar", @@ -1232,7 +1212,6 @@ "common.no": "Não", "common.on": "Ligado", "common.off": "Desligado", - "setup.title": "Escolha os motores de conversão", "setup.description": "Imagens, PDF, documentos do Office, HWP, e-books, arquivos compactados, fontes, CAD e modelos 3D já convertem.\nBaixe motores só para vídeo e áudio ou formatos raros.", "setup.note": "Você pode instalar ou remover motores a qualquer momento em Configurações → Motores.", @@ -1395,7 +1374,6 @@ "watch.status_starting": "Iniciando…", "watch.status_error": "Não é possível monitorar esta pasta: {0}", "watch.cli_hint": "Também na linha de comando: filee watch --to ", - "preset.sheet_studio": "PDF + Editar", "sheet_studio.title": "Estúdio de impressão de planilhas", "sheet_studio.sheets": "Planilhas e ordem de saída", @@ -1435,7 +1413,6 @@ "sheet_studio.apply_fix": "Aplicar layout sugerido", "sheet_studio.add_region": "+ Adicionar outra área", "sheet_studio.region_name": "{0} · área {1}", - "whats_new.title": "Novidades do Filee {0}", "whats_new.previous": "Anterior", "whats_new.next": "Próximo", @@ -1513,6 +1490,28 @@ "engines.ollama.complete": "Concluído!", "engines.ollama.failed": "Falha:", "engines.ollama.error": "Erro:", + "preset.upscale": "Aumentar resolução da imagem", + "preset.upscale.donut": "Super-resolução", + "engine.upscaler.name": "Modelos de qualidade de imagem", + "engine.upscaler.description": "Aprimoramento e super-resolução de imagem com IA até 4× usando modelos locais NCNN.", + "engines.upscaler.modal_description": "Baixe modelos locais de super-resolução para aumentar imagens em até 4×. Todo o processamento ocorre 100% offline no seu computador.", + "upscale.model": "Modelo", + "upscale.scale": "Fator de escala", + "upscale.output_format": "Formato de saída", + "upscale.format_same": "Mesmo formato de origem ({0})", + "upscale.resolution_label": "Prévia da resolução", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "Aumentar agora", + "upscale.success": "Aumento de resolução concluído com sucesso.", + "error.upscale_failed": "Falha no aumento de resolução: {0}", + "engines.package.upscayl-bin.name": "Upscayl (upscaler de imagem por IA)", + "engines.package.upscayl-bin.description": "Upscaling e super-resolução de imagens por IA em até 4× com modelos NCNN locais.\nOs modelos são baixados na primeira vez que são usados.", + "error.upscale_needs_engine": "Instale o Upscayl em Configurações → Motores para usar o upscaling por IA.", + "error.upscale_needs_vulkan": "O upscaling requer uma placa de vídeo (GPU) compatível com Vulkan.", + "upscale.model_ready": "Pronto", + "upscale.model_download_size": "Download de {0} MB", + "upscale.downloading_model": "Baixando modelo...", + "engines.model.delete_confirm": "Excluir o modelo {0}? Você pode baixá-lo novamente a qualquer momento.", "home.queue_error": "Não foi possível converter {0}. A lista mantém os arquivos.", "home.queue_selected": "selecionado", "home.queue_remove_file": "Remover {0}", diff --git a/src/Filee.App/Assets/i18n/pt-PT.json b/src/Filee.App/Assets/i18n/pt-PT.json index d19625bf..08c39456 100644 --- a/src/Filee.App/Assets/i18n/pt-PT.json +++ b/src/Filee.App/Assets/i18n/pt-PT.json @@ -1,7 +1,6 @@ { "app.name": "Filee", "app.tagline": "Arrasta, larga, convertido.", - "fields.title": "Áreas", "fields.description": "Desativa as áreas em que não trabalhas. Os formatos que pertencem apenas a áreas desativadas não aparecem nos segmentos da barra circular, em «Predefinições disponíveis» de Definições → Barra circular, na lista de Definições → Predefinições nem na lista «Converter para» de uma predefinição. O Filee continua a reconhecer os respetivos ficheiros e a linha de comandos continua a converter para esses formatos. Um ficheiro largado continua a apresentar os formatos das suas próprias áreas.", "fields.formats": "Formatos: {0}", @@ -18,7 +17,6 @@ "fields.name.video-pro": "Produção de vídeo e emissão", "fields.name.audio-pro": "Áudio profissional", "fields.name.legacy": "Formatos antigos", - "nav.home": "Página inicial", "nav.general": "Geral", "nav.triggers": "Atalhos", @@ -31,7 +29,6 @@ "nav.about": "Acerca de", "nav.collapse": "Recolher barra lateral", "nav.expand": "Expandir barra lateral", - "tray.open": "Abrir o Filee", "tray.pause": "Pausar atalhos", "tray.resume": "Retomar atalhos", @@ -43,7 +40,6 @@ "close.tray": "Manter na área de notificação", "close.quit": "Sair", "close.dont_ask": "Não voltar a perguntar", - "home.title": "Página inicial", "home.drop_title": "Larga os ficheiros aqui", "home.drop_select": "Faz duplo clique para selecionar ficheiros", @@ -72,7 +68,6 @@ "home.undone": "Anulado: os resultados estão na Reciclagem ou no Lixo", "home.undone_original": "Anulado: os resultados estão na Reciclagem ou no Lixo, tal como os originais que substituíram", "home.undo_failed": "Não foi possível mover {0} para a Reciclagem ou para o Lixo: {1}", - "donut.center_drag": "Arrasta ficheiros para aqui", "donut.files_same": "{0} × {1}", "donut.files_mixed": "{0} ficheiros", @@ -88,7 +83,6 @@ "donut.group_caption": "Grupo de {0}", "donut.flip_to_back": "{0} verso", "donut.flip_to_front": "{0} frente", - "toast.converting": "{0} → {1}", "toast.done": "Concluído", "toast.done_errors": "Falhas: {0} de {1}", @@ -101,7 +95,6 @@ "toast.get_fonts": "Obter tipos de letra", "download.badge": "{0}/s · {1}", "download.badge_files": "{0} · {1} de {2} ficheiros", - "error.unsupported_source": "Este tipo de ficheiro não é suportado", "error.empty_file": "O ficheiro está vazio (0 bytes). Um ficheiro criado com «Novo» no menu de contexto fica vazio até o abrires na respetiva aplicação e o guardares", "error.p7m_unreadable": "Este não é um ficheiro assinado .p7m que o Filee consiga abrir, ou está danificado", @@ -194,7 +187,6 @@ "notice.geo_field_names": "Os nomes dos campos Shapefile admitem, no máximo, 10 carateres: alguns foram abreviados", "notice.geo_shapefile_split": "Um Shapefile guarda apenas um tipo de geometria, pelo que os pontos, linhas e polígonos foram guardados em ficheiros separados", "notice.geo_heights_dropped": "Alguns pontos não têm altura e um Shapefile não pode omiti-la apenas em alguns, pelo que todas as alturas foram omitidas", - "engine.reason.not_installed": "Não instalado", "engine.reason.error": "Erro na verificação", "engine.reason.unsupported_platform": "Indisponível nesta plataforma", @@ -257,7 +249,6 @@ "engine.calibre.description": "Formatos raros de livros digitais (LIT, LRF, CHM, PDB, RB…) e saída para Kindle (MOBI, AZW3).\nAcima do gerador HWPX, também cria PDF de livros digitais (muito mais depressa para livros longos).", "engine.libreoffice.description": "Lê formatos antigos e menos comuns: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nTambém guarda em DOC, XLS, PPT, ODT, ODP, modelos OpenDocument e EMF.", "engine.translator.description": "Traduz documentos e e-books localmente através de modelos offline do Ollama.\nTambém traduz legendas SRT e WebVTT: apenas o texto de cada legenda é enviado ao modelo; os números e os tempos das legendas mantêm-se.", - "profile.images": "Imagens", "profile.pdf": "PDF", "profile.office": "Documentos", @@ -277,7 +268,6 @@ "profile.models": "Modelos 3D", "profile.geo": "Dados geoespaciais", "profile.mixed": "Ficheiros mistos", - "preset.crop": "Recortar", "crop.save": "Recortar e guardar", "crop.reset": "Repor", @@ -404,7 +394,6 @@ "clip.silence_removed": "Silêncio removido: {0}", "clip.no_silence": "Não foi encontrado silêncio", "clip.cannot_play": "Não é possível reproduzir o excerto", - "preset.half_size": "Metade do tamanho", "preset.half_size.donut": "Tamanho 50%", "preset.image_edit": "Editar imagens", @@ -519,7 +508,6 @@ "preset.merge_audio": "Juntar áudio", "preset.merge_images": "Juntar imagens", "preset.remove_metadata": "Remover metadados", - "format.folder": "Pasta", "format.category.Image": "Imagem", "format.category.Pdf": "PDF", @@ -539,7 +527,6 @@ "format.category.Data": "Dados", "format.category.Model3D": "Modelo 3D", "format.category.Geo": "Dados geoespaciais", - "general.title": "Geral", "general.language": "Idioma", "general.language_auto": "Predefinição do sistema", @@ -622,7 +609,6 @@ "general.reset_library": "Repor valores de origem", "general.reset_confirm": "Substituir todas as predefinições e perfis da barra pelos de origem?", "general.imported": "Itens importados: {0}", - "triggers.title": "Atalhos", "triggers.description": "Escolhe como abrir a barra circular. Podes adicionar os gestos que quiseres.", "triggers.macos_permissions": "Autoriza o Filee em Definições do Sistema > Privacidade e Segurança > Acessibilidade.\nSe o Filee já estiver ativado mas os gestos não funcionarem, remove-o com − e volta a adicioná-lo com +.\nO Finder também pede permissão de Automatização na primeira utilização de um atalho de seleção.", @@ -680,7 +666,6 @@ "triggers.excluded_none_running": "Nenhuma outra aplicação tem uma janela aberta.", "triggers.excluded_note": "Os gestos e atalhos não têm efeito enquanto o ponteiro estiver sobre estas aplicações.", "triggers.paused": "Os atalhos estão em pausa", - "toolbar.title": "Barra circular", "toolbar.step_profile": "1. Escolhe um perfil", "toolbar.step_arrange": "2. Organiza as predefinições", @@ -748,18 +733,17 @@ "toolbar.mixed_covered": "«{0}» aparece primeiro e abrange todas as extensões selecionadas aqui, pelo que este perfil nunca é utilizado.", "toolbar.mixed_others": "As restantes misturas de ficheiros abrem «{0}».", "toolbar.mixed_others_self": "Não resta nenhum perfil misto sem opções selecionadas, pelo que este também é utilizado para todas as outras misturas.", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ Mesmo formato", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "Formatos aceites: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} a mais · Shift para mostrar todos", - "toolbar.chip_tip.shift_all": "Shift para mostrar todos", - "toolbar.chip_tip.any_file": "Qualquer ficheiro", - "tags.remove": "Remover", - + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ Mesmo formato", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "Formatos aceites: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} a mais · Shift para mostrar todos", + "toolbar.chip_tip.shift_all": "Shift para mostrar todos", + "toolbar.chip_tip.any_file": "Qualquer ficheiro", + "tags.remove": "Remover", "presets.title": "Predefinições", "presets.add": "Nova", "presets.add_empty": "Predefinição vazia", @@ -1083,7 +1067,6 @@ "presets.delete_confirm": "Eliminar «{0}»? Também será removida de todas as barras e grupos.", "presets.name_required": "Introduz um nome", "presets.select": "Seleciona uma predefinição para a editar", - "theme.title": "Tema", "theme.style": "Estilo do tema", "theme.style.Soft": "Filee Soft", @@ -1120,7 +1103,6 @@ "theme.preview": "Pré-visualização", "theme.preview_toggle": "Interruptor de exemplo (só pré-visualização, não é uma definição)", "theme.preview_toggle_hint": "Mostra o aspeto dos interruptores neste tema. Não altera nenhuma definição.", - "engines.title": "Motores de conversão", "engines.description": "O Filee escolhe o melhor motor disponível para cada conversão.\nQuando vários conseguem fazer o mesmo, tem prioridade o que estiver mais acima.", "engines.available": "Disponível", @@ -1197,7 +1179,6 @@ "engines.state.unpacking": "A instalar…", "engines.state.installed": "Instalado", "engines.state.failed": "Falhou: {0}", - "about.title": "Acerca de", "about.updates": "Atualizações", "common.search": "Pesquisar", @@ -1222,7 +1203,6 @@ "feedback.notice_text": "Uma estrela no GitHub ajuda outras pessoas a descobrir o Filee.\nTambém são bem-vindas sugestões e comunicações de problemas.", "feedback.star": "Dar uma estrela no GitHub", "feedback.issue": "Comunicar um problema ou ideia", - "common.ok": "OK", "common.cancel": "Cancelar", "common.save": "Guardar", @@ -1232,7 +1212,6 @@ "common.no": "Não", "common.on": "Ativado", "common.off": "Desativado", - "setup.title": "Escolhe os motores de conversão", "setup.description": "Já é possível converter imagens, PDF, documentos Office, HWP, livros digitais, ficheiros comprimidos, tipos de letra, CAD e modelos 3D.\nDescarrega motores apenas para vídeo e áudio ou formatos raros.", "setup.note": "Podes instalar ou remover motores a qualquer momento em Definições → Motores.", @@ -1395,7 +1374,6 @@ "watch.status_starting": "A iniciar…", "watch.status_error": "Não é possível monitorizar esta pasta: {0}", "watch.cli_hint": "Também na linha de comandos: filee watch --to ", - "preset.sheet_studio": "PDF + Editar", "sheet_studio.title": "Estúdio de impressão", "sheet_studio.sheets": "Folhas e ordem de saída", @@ -1435,7 +1413,6 @@ "sheet_studio.apply_fix": "Aplicar disposição sugerida", "sheet_studio.add_region": "+ Adicionar outra região", "sheet_studio.region_name": "{0} · região {1}", - "whats_new.title": "Novidades do Filee {0}", "whats_new.previous": "Anterior", "whats_new.next": "Seguinte", @@ -1513,6 +1490,28 @@ "engines.ollama.complete": "Concluído!", "engines.ollama.failed": "Falha:", "engines.ollama.error": "Erro:", + "preset.upscale": "Aumentar resolução da imagem", + "preset.upscale.donut": "Super-resolução", + "engine.upscaler.name": "Modelos de qualidade de imagem", + "engine.upscaler.description": "Melhoramento e super-resolução de imagem com IA até 4× com modelos locais NCNN.", + "engines.upscaler.modal_description": "Transfira modelos locais de super-resolução para aumentar imagens até 4×. Todo o processamento corre 100% offline no seu computador.", + "upscale.model": "Modelo", + "upscale.scale": "Fator de escala", + "upscale.output_format": "Formato de saída", + "upscale.format_same": "Mesmo formato de origem ({0})", + "upscale.resolution_label": "Pré-visualização da resolução", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "Aumentar agora", + "upscale.success": "Super-resolução concluída com sucesso.", + "error.upscale_failed": "Falha no aumento de resolução: {0}", + "engines.package.upscayl-bin.name": "Upscayl (upscaler de imagem por IA)", + "engines.package.upscayl-bin.description": "Upscaling e super-resolução de imagens por IA até 4× com modelos NCNN locais.\nOs modelos são descarregados na primeira utilização.", + "error.upscale_needs_engine": "Instale o Upscayl em Definições → Motores para usar o upscaling por IA.", + "error.upscale_needs_vulkan": "O upscaling requer uma placa gráfica (GPU) compatível com Vulkan.", + "upscale.model_ready": "Pronto", + "upscale.model_download_size": "Transferência de {0} MB", + "upscale.downloading_model": "A descarregar modelo...", + "engines.model.delete_confirm": "Eliminar o modelo {0}? Pode descarregá-lo novamente a qualquer momento.", "home.queue_error": "Não foi possível converter {0}. A lista mantém os ficheiros.", "home.queue_selected": "selecionado", "home.queue_remove_file": "Remover {0}", diff --git a/src/Filee.App/Assets/i18n/ru.json b/src/Filee.App/Assets/i18n/ru.json index 251c38fa..83786f15 100644 --- a/src/Filee.App/Assets/i18n/ru.json +++ b/src/Filee.App/Assets/i18n/ru.json @@ -1,7 +1,6 @@ { "app.name": "Filee", "app.tagline": "Перетащил — и готово.", - "fields.title": "Области", "fields.description": "Отключите области, с которыми вы не работаете. Форматы, относящиеся только к отключённым областям, не отображаются в секторах кругового меню, в разделе «Доступные пресеты» в «Настройки → Круговое меню», в списке в «Настройки → Пресеты» и в списке «Конвертировать в» у пресета. Filee по-прежнему распознаёт их файлы, а командная строка может конвертировать в эти форматы. Для перетащенного файла по-прежнему предлагаются форматы его собственных областей.", "fields.formats": "Форматы: {0}", @@ -18,7 +17,6 @@ "fields.name.video-pro": "Видеопроизводство и вещание", "fields.name.audio-pro": "Профессиональное аудио", "fields.name.legacy": "Старые форматы", - "nav.home": "Главная", "nav.general": "Общие", "nav.triggers": "Горячие клавиши", @@ -31,7 +29,6 @@ "nav.about": "О программе", "nav.collapse": "Свернуть боковую панель", "nav.expand": "Развернуть боковую панель", - "tray.open": "Открыть Filee", "tray.pause": "Приостановить горячие клавиши", "tray.resume": "Возобновить горячие клавиши", @@ -43,7 +40,6 @@ "close.tray": "Оставить в трее", "close.quit": "Выйти", "close.dont_ask": "Больше не спрашивать", - "home.title": "Главная", "home.drop_title": "Перетащите файлы сюда", "home.drop_select": "Дважды щелкните, чтобы выбрать файлы", @@ -72,7 +68,6 @@ "home.undone": "Отменено: результаты в Корзине", "home.undone_original": "Отменено: результаты в Корзине, как и оригиналы, которые они заменили", "home.undo_failed": "Не удалось переместить {0} в Корзину: {1}", - "donut.center_drag": "Перетащите сюда", "donut.files_same": "{0} × {1}", "donut.files_mixed": "Файлов: {0}", @@ -88,7 +83,6 @@ "donut.group_caption": "Группа: {0}", "donut.flip_to_back": "{0} оборот", "donut.flip_to_front": "{0} лицо", - "toast.converting": "{0} → {1}", "toast.done": "Готово", "toast.done_errors": "Не удалось: {0} из {1}", @@ -101,7 +95,6 @@ "toast.get_fonts": "Получить шрифты", "download.badge": "{0}/с · {1}", "download.badge_files": "{0} · файл {1} из {2}", - "error.unsupported_source": "Этот тип файлов не поддерживается", "error.empty_file": "Файл пустой (0 байт). Файл, созданный через «Создать» в контекстном меню, остаётся пустым, пока его не откроют в программе и не сохранят", "error.p7m_unreadable": "Это не подписанный файл .p7m, который может открыть Filee, или он повреждён", @@ -194,7 +187,6 @@ "notice.geo_field_names": "Имена полей Shapefile не длиннее 10 символов: некоторые сокращены", "notice.geo_shapefile_split": "Shapefile хранит фигуры только одного вида, поэтому точки, линии и полигоны сохранены в отдельные файлы", "notice.geo_heights_dropped": "У некоторых точек нет высоты, а Shapefile не может пропустить высоту только у одной точки, поэтому убраны все высоты", - "engine.reason.not_installed": "Не установлен", "engine.reason.error": "Ошибка при проверке", "engine.reason.unsupported_platform": "Недоступен на этой платформе", @@ -257,7 +249,6 @@ "engine.calibre.description": "Редкие форматы электронных книг (LIT, LRF, CHM, PDB, RB…) и вывод для Kindle (MOBI, AZW3).\nЕсли стоит выше HWPX writer, он же делает PDF из электронных книг (для длинных книг намного быстрее).", "engine.libreoffice.description": "Читает старые и редкие форматы: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nА ещё сохраняет в DOC, XLS, PPT, ODT, ODP, шаблоны OpenDocument и EMF.", "engine.translator.description": "Переводит документы и электронные книги локально с помощью офлайн-моделей Ollama.\nТакже переводит субтитры SRT и WebVTT: в модель отправляется только текст реплик, номера и тайм-коды сохраняются.", - "profile.images": "Изображения", "profile.pdf": "PDF", "profile.office": "Документы", @@ -277,7 +268,6 @@ "profile.models": "3D-модели", "profile.geo": "Геоданные", "profile.mixed": "Разные файлы", - "preset.crop": "Обрезать", "crop.save": "Обрезать и сохранить", "crop.reset": "Сбросить", @@ -404,7 +394,6 @@ "clip.silence_removed": "Убрано тихих мест: {0}", "clip.no_silence": "Тишины не найдено", "clip.cannot_play": "Клип не воспроизводится", - "preset.half_size": "Вдвое меньше", "preset.half_size.donut": "Вдвое меньше", "preset.image_edit": "Изменить изображения", @@ -519,7 +508,6 @@ "preset.merge_audio": "Объединить аудио", "preset.merge_images": "Объединить изображения", "preset.remove_metadata": "Удалить метаданные", - "format.folder": "Папка", "format.category.Image": "Изображение", "format.category.Pdf": "PDF", @@ -539,7 +527,6 @@ "format.category.Data": "Данные", "format.category.Model3D": "3D-модель", "format.category.Geo": "Геоданные", - "general.title": "Общие", "general.language": "Язык", "general.language_auto": "Как в системе", @@ -622,7 +609,6 @@ "general.reset_library": "Вернуть по умолчанию", "general.reset_confirm": "Заменить все пресеты и профили кругового меню на стандартные?", "general.imported": "Импортировано: {0}", - "triggers.title": "Горячие клавиши", "triggers.description": "Выберите, как открывается круговое меню. Жестов можно добавить сколько угодно.", "triggers.macos_permissions": "Разрешите Filee в разделе «Системные настройки > Конфиденциальность и безопасность > Универсальный доступ».\nЕсли Filee уже включён, но жесты не запускаются, удалите его кнопкой − и добавьте снова кнопкой +.\nFinder также спросит разрешение на автоматизацию, когда вы впервые воспользуетесь сочетанием для выделенных файлов.", @@ -680,7 +666,6 @@ "triggers.excluded_none_running": "Нет других программ с открытыми окнами.", "triggers.excluded_note": "Жесты и сочетания ничего не делают, пока указатель над этими программами.", "triggers.paused": "Горячие клавиши приостановлены", - "toolbar.title": "Круговое меню", "toolbar.step_profile": "1. Выберите профиль", "toolbar.step_arrange": "2. Расставьте пресеты", @@ -748,18 +733,17 @@ "toolbar.mixed_covered": "«{0}» стоит выше и допускает все отмеченные здесь расширения, поэтому этот профиль никогда не используется.", "toolbar.mixed_others": "Остальные смеси файлов открывают «{0}».", "toolbar.mixed_others_self": "Не осталось смешанного профиля без отметок, поэтому этот используется и для всех остальных смесей.", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ Тот же формат", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "Поддерживаемые форматы: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} дополнительно · Shift — показать все", - "toolbar.chip_tip.shift_all": "Shift — показать все", - "toolbar.chip_tip.any_file": "Любой файл", - "tags.remove": "Убрать", - + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ Тот же формат", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "Поддерживаемые форматы: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} дополнительно · Shift — показать все", + "toolbar.chip_tip.shift_all": "Shift — показать все", + "toolbar.chip_tip.any_file": "Любой файл", + "tags.remove": "Убрать", "presets.title": "Пресеты", "presets.add": "Новый", "presets.add_empty": "Пустой пресет", @@ -1083,7 +1067,6 @@ "presets.delete_confirm": "Удалить «{0}»? Он также пропадёт из всех круговых меню и групп.", "presets.name_required": "Введите название", "presets.select": "Выберите пресет, чтобы изменить его", - "theme.title": "Тема", "theme.style": "Стиль темы", "theme.style.Soft": "Filee Soft", @@ -1120,7 +1103,6 @@ "theme.preview": "Предпросмотр", "theme.preview_toggle": "Пример переключателя (только предпросмотр, не настройка)", "theme.preview_toggle_hint": "Показывает, как выглядят переключатели в этой теме. Не меняет никаких настроек.", - "engines.title": "Движки конвертации", "engines.description": "Для каждой конвертации Filee выбирает лучший доступный движок.\nЕсли с задачей справляются несколько, выбирается тот, что выше.", "engines.available": "Готовы", @@ -1197,7 +1179,6 @@ "engines.state.unpacking": "Устанавливается…", "engines.state.installed": "Установлен", "engines.state.failed": "Ошибка: {0}", - "about.title": "О программе", "about.updates": "Обновления", "common.search": "Поиск", @@ -1222,7 +1203,6 @@ "feedback.notice_text": "Звезда на GitHub помогает другим найти Filee.\nБудем рады и сообщениям о проблемах, и идеям.", "feedback.star": "Поставить звезду на GitHub", "feedback.issue": "Сообщить о проблеме или идее", - "common.ok": "OK", "common.cancel": "Отмена", "common.save": "Сохранить", @@ -1232,7 +1212,6 @@ "common.no": "Нет", "common.on": "Вкл.", "common.off": "Выкл.", - "setup.title": "Выберите движки конвертации", "setup.description": "Картинки, PDF, документы Office, HWP, электронные книги, архивы, шрифты, CAD и 3D-модели уже конвертируются.\nДвижки нужно скачивать только для видео, аудио и редких форматов.", "setup.note": "Движки можно установить или удалить в любой момент в разделе «Настройки → Движки».", @@ -1395,7 +1374,6 @@ "watch.status_starting": "Запуск…", "watch.status_error": "Не удаётся следить за этой папкой: {0}", "watch.cli_hint": "Также из командной строки: filee watch <папка> --to <формат>", - "preset.sheet_studio": "PDF + Редактор", "sheet_studio.title": "Подготовка таблицы к PDF", "sheet_studio.sheets": "Листы и порядок страниц", @@ -1435,7 +1413,6 @@ "sheet_studio.apply_fix": "Применить рекомендуемую разметку", "sheet_studio.add_region": "+ Добавить ещё область", "sheet_studio.region_name": "{0} · область {1}", - "whats_new.title": "Что нового в Filee {0}", "whats_new.previous": "Назад", "whats_new.next": "Далее", @@ -1513,6 +1490,28 @@ "engines.ollama.complete": "Готово!", "engines.ollama.failed": "Ошибка:", "engines.ollama.error": "Ошибка:", + "preset.upscale": "Увеличить разрешение изображения", + "preset.upscale.donut": "Увеличить", + "engine.upscaler.name": "Модели улучшения качества", + "engine.upscaler.description": "ИИ-увеличение разрешения изображений до 4× с помощью локальных моделей NCNN.", + "engines.upscaler.modal_description": "Загрузите локальные модели сверхвысокого разрешения для масштабирования изображений до 4×. Обработка на 100% автономна.", + "upscale.model": "Модель", + "upscale.scale": "Масштаб", + "upscale.output_format": "Формат вывода", + "upscale.format_same": "Исходный формат ({0})", + "upscale.resolution_label": "Предпросмотр разрешения", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "Увеличить сейчас", + "upscale.success": "Увеличение успешно завершено.", + "error.upscale_failed": "Ошибка при увеличении: {0}", + "engines.package.upscayl-bin.name": "Upscayl (ИИ-апскейлер изображений)", + "engines.package.upscayl-bin.description": "Апскейлинг и суперразрешение изображений до 4× с помощью локальных моделей NCNN.\nМодели скачиваются при первом использовании.", + "error.upscale_needs_engine": "Установите Upscayl в меню Настройки → Движки для использования ИИ-апскейлинга.", + "error.upscale_needs_vulkan": "Для масштабирования требуется видеокарта (GPU) с поддержкой Vulkan.", + "upscale.model_ready": "Готово", + "upscale.model_download_size": "Загрузка {0} МБ", + "upscale.downloading_model": "Загрузка модели...", + "engines.model.delete_confirm": "Удалить модель {0}? Вы сможете скачать её снова в любой момент.", "home.queue_error": "Не удалось конвертировать: {0}. Список сохраняет файлы.", "home.queue_selected": "выбрано", "home.queue_remove_file": "Удалить {0}", diff --git a/src/Filee.App/Assets/i18n/th.json b/src/Filee.App/Assets/i18n/th.json index ed884df3..9a19d20a 100644 --- a/src/Filee.App/Assets/i18n/th.json +++ b/src/Filee.App/Assets/i18n/th.json @@ -17,7 +17,6 @@ "fields.name.video-pro": "ผลิตวิดีโอและออกอากาศ", "fields.name.audio-pro": "เสียงระดับมืออาชีพ", "fields.name.legacy": "รูปแบบเก่า", - "nav.home": "หน้าแรก", "nav.general": "ทั่วไป", "nav.triggers": "ปุ่มลัด", @@ -734,17 +733,17 @@ "toolbar.mixed_covered": "“{0}” อยู่ก่อนหน้าและครอบคลุมทุกนามสกุลที่เลือกไว้ที่นี่ โปรไฟล์นี้จึงจะไม่ได้ถูกนำมาใช้", "toolbar.mixed_others": "ไฟล์รวมประเภทแบบอื่นๆ จะเปิด “{0}”", "toolbar.mixed_others_self": "ไม่มีโปรไฟล์ไฟล์รวมอันอื่นที่ไม่ได้เลือกอะไรไว้ ดังนั้นโปรไฟล์นี้จะถูกใช้กับไฟล์รวมแบบอื่นๆ ทั้งหมดด้วย", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ รูปแบบเดียวกัน", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "รูปแบบที่รองรับ: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} เพิ่มเติม · กด Shift เพื่อแสดงทั้งหมด", - "toolbar.chip_tip.shift_all": "กด Shift เพื่อแสดงทั้งหมด", - "toolbar.chip_tip.any_file": "ไฟล์ใดก็ได้", - "tags.remove": "ลบออก", + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ รูปแบบเดียวกัน", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "รูปแบบที่รองรับ: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} เพิ่มเติม · กด Shift เพื่อแสดงทั้งหมด", + "toolbar.chip_tip.shift_all": "กด Shift เพื่อแสดงทั้งหมด", + "toolbar.chip_tip.any_file": "ไฟล์ใดก็ได้", + "tags.remove": "ลบออก", "presets.title": "พรีเซ็ต", "presets.add": "สร้างใหม่", "presets.add_empty": "พรีเซ็ตว่าง", @@ -1375,7 +1374,6 @@ "watch.status_starting": "กำลังเริ่มต้น…", "watch.status_error": "ไม่สามารถเฝ้าติดตามโฟลเดอร์นี้ได้: {0}", "watch.cli_hint": "ใช้งานผ่านบรรทัดคำสั่งได้เช่นกัน: filee watch <โฟลเดอร์> --to <ฟอร์แมต>", - "preset.sheet_studio": "PDF + แก้ไข", "sheet_studio.title": "สตูดิโอพิมพ์สเปรดชีต", "sheet_studio.sheets": "ชีตและลำดับผลลัพธ์", @@ -1415,7 +1413,6 @@ "sheet_studio.apply_fix": "ใช้เลย์เอาต์ที่แนะนำ", "sheet_studio.add_region": "+ เพิ่มพื้นที่อื่น", "sheet_studio.region_name": "{0} · พื้นที่ {1}", - "whats_new.title": "มีอะไรใหม่ใน Filee {0}", "whats_new.previous": "ก่อนหน้า", "whats_new.next": "ถัดไป", @@ -1493,6 +1490,28 @@ "engines.ollama.complete": "เสร็จสิ้น!", "engines.ollama.failed": "ล้มเหลว:", "engines.ollama.error": "ข้อผิดพลาด:", + "preset.upscale": "เพิ่มความคมชัดและขยายภาพ", + "preset.upscale.donut": "ขยายความคมชัด", + "engine.upscaler.name": "โมเดลเพิ่มคุณภาพรูปภาพ", + "engine.upscaler.description": "การขยายภาพความละเอียดสูงด้วย AI สูงสุด 4 เท่า ด้วยโมเดล NCNN บนเครื่อง", + "engines.upscaler.modal_description": "ดาวน์โหลดโมเดล AI ขยายความคมชัดของภาพได้สูงสุด 4 เท่า การประมวลผลทำงานแบบออฟไลน์ 100% บนเครื่องของคุณ", + "upscale.model": "โมเดล", + "upscale.scale": "อัตราส่วนการขยาย", + "upscale.output_format": "รูปแบบผลลัพธ์", + "upscale.format_same": "รูปแบบเดิมเหมือนต้นฉบับ ({0})", + "upscale.resolution_label": "ตัวอย่างความละเอียด", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "ขยายภาพทันที", + "upscale.success": "เพิ่มความคมชัดเรียบร้อยแล้ว", + "error.upscale_failed": "การขยายภาพล้มเหลว: {0}", + "engines.package.upscayl-bin.name": "Upscayl (เพิ่มความละเอียดภาพด้วย AI)", + "engines.package.upscayl-bin.description": "เพิ่มความละเอียดภาพและซูเปอร์เรโซลูชันสูงสุด 4 เท่าด้วยโมเดล NCNN บนเครื่อง\nโมเดลจะดาวน์โหลดเมื่อใช้ครั้งแรก", + "error.upscale_needs_engine": "ติดตั้ง Upscayl ในการตั้งค่า → เครื่องมือแปลงไฟล์ เพื่อใช้งานการเพิ่มความละเอียดภาพด้วย AI", + "error.upscale_needs_vulkan": "การเพิ่มความละเอียดต้องใช้การ์ดกราฟิก (GPU) ที่รองรับ Vulkan", + "upscale.model_ready": "พร้อมใช้งาน", + "upscale.model_download_size": "ดาวน์โหลด {0} MB", + "upscale.downloading_model": "กำลังดาวน์โหลดโมเดล...", + "engines.model.delete_confirm": "ลบโมเดล {0} หรือไม่ คุณสามารถดาวน์โหลดใหม่ได้ตลอดเวลา", "home.queue_error": "แปลง {0} ไม่สำเร็จ รายการยังเก็บไฟล์ไว้", "home.queue_selected": "เลือกอยู่", "home.queue_remove_file": "นำ {0} ออก", diff --git a/src/Filee.App/Assets/i18n/tr.json b/src/Filee.App/Assets/i18n/tr.json index b4260da4..14b7c020 100644 --- a/src/Filee.App/Assets/i18n/tr.json +++ b/src/Filee.App/Assets/i18n/tr.json @@ -17,7 +17,6 @@ "fields.name.video-pro": "Video prodüksiyonu ve yayıncılık", "fields.name.audio-pro": "Profesyonel ses", "fields.name.legacy": "Eski biçimler", - "nav.home": "Ana sayfa", "nav.general": "Genel", "nav.triggers": "Kısayollar", @@ -734,17 +733,17 @@ "toolbar.mixed_covered": "“{0}” önce geliyor ve burada işaretli tüm uzantıları kabul ediyor; bu profil hiç kullanılmaz.", "toolbar.mixed_others": "Diğer karışık dosya seçimleri “{0}” profilini açar.", "toolbar.mixed_others_self": "Hiçbir karışık profil tamamen işaretsiz kalmadığı için bu profil diğer tüm karışık seçimlerde de kullanılır.", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ Aynı biçim", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "Desteklenen biçimler: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} ek · Shift tümünü gösterir", - "toolbar.chip_tip.shift_all": "Shift tümünü gösterir", - "toolbar.chip_tip.any_file": "Herhangi bir dosya", - "tags.remove": "Kaldır", + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ Aynı biçim", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "Desteklenen biçimler: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} ek · Shift tümünü gösterir", + "toolbar.chip_tip.shift_all": "Shift tümünü gösterir", + "toolbar.chip_tip.any_file": "Herhangi bir dosya", + "tags.remove": "Kaldır", "presets.title": "Ön ayarlar", "presets.add": "Yeni", "presets.add_empty": "Boş ön ayar", @@ -1491,6 +1490,28 @@ "engines.ollama.complete": "Tamamlandı!", "engines.ollama.failed": "Başarısız:", "engines.ollama.error": "Hata:", + "preset.upscale": "Görüntü Kalitesini Yükselt", + "preset.upscale.donut": "Büyüt ve İyileştir", + "engine.upscaler.name": "Görüntü Kalitesi Modelleri", + "engine.upscaler.description": "Yerel NCNN modelleriyle 4 kata kadar yapay zeka süper çözünürlük ve görüntü büyütme.", + "engines.upscaler.modal_description": "Görüntüleri 4 kata kadar büyütmek için yerel yapay zeka modellerini indirin. Tüm işlemler %100 çevrimdışı çalışır.", + "upscale.model": "Model", + "upscale.scale": "Ölçek Faktörü", + "upscale.output_format": "Çıkış Formatı", + "upscale.format_same": "Kaynak ile aynı format ({0})", + "upscale.resolution_label": "Çözünürlük Önizlemesi", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "Şimdi Büyüt", + "upscale.success": "Görüntü büyütme başarıyla tamamlandı.", + "error.upscale_failed": "Büyütme başarısız: {0}", + "engines.package.upscayl-bin.name": "Upscayl (yapay zeka görüntü yükseltici)", + "engines.package.upscayl-bin.description": "Yerel NCNN modelleriyle 4 kata kadar yapay zeka görüntü yükseltme ve süper çözünürlük.\nModeller ilk kullanıldıklarında indirilir.", + "error.upscale_needs_engine": "Yapay zeka yükseltmesini kullanmak için Ayarlar → Dönüştürme motorları bölümünden Upscayl kurun.", + "error.upscale_needs_vulkan": "Yükseltme için Vulkan uyumlu bir grafik kartı (GPU) gerekir.", + "upscale.model_ready": "Hazır", + "upscale.model_download_size": "{0} MB indirme", + "upscale.downloading_model": "Model indiriliyor...", + "engines.model.delete_confirm": "{0} modeli silinsin mi? İstediğiniz zaman tekrar indirebilirsiniz.", "home.queue_error": "{0} dönüştürülemedi. Liste dosyaları tutar.", "home.queue_selected": "seçili", "home.queue_remove_file": "{0} dosyasını kaldır", diff --git a/src/Filee.App/Assets/i18n/vi.json b/src/Filee.App/Assets/i18n/vi.json index e2b39cce..0b28b57d 100644 --- a/src/Filee.App/Assets/i18n/vi.json +++ b/src/Filee.App/Assets/i18n/vi.json @@ -1,7 +1,6 @@ { "app.name": "Filee", "app.tagline": "Kéo, thả, xong.", - "fields.title": "Lĩnh vực", "fields.description": "Tắt các lĩnh vực bạn không làm việc. Định dạng chỉ thuộc các lĩnh vực đã tắt sẽ không xuất hiện trong các phần của vòng tròn, trong “Cấu hình sẵn có thể dùng” ở Cài đặt → Thanh công cụ vòng tròn, trong danh sách ở Cài đặt → Cấu hình sẵn và trong danh sách “Chuyển đổi sang” của cấu hình sẵn. Filee vẫn nhận ra các tệp của định dạng đó, dòng lệnh vẫn chuyển đổi sang được, và tệp bạn thả vào vẫn hiện các định dạng thuộc lĩnh vực của chính nó.", "fields.formats": "Định dạng: {0}", @@ -18,7 +17,6 @@ "fields.name.video-pro": "Sản xuất & phát sóng video", "fields.name.audio-pro": "Âm thanh chuyên nghiệp", "fields.name.legacy": "Định dạng cũ", - "nav.home": "Trang chủ", "nav.general": "Chung", "nav.triggers": "Phím tắt", @@ -31,7 +29,6 @@ "nav.about": "Giới thiệu", "nav.collapse": "Thu gọn thanh bên", "nav.expand": "Mở rộng thanh bên", - "tray.open": "Mở Filee", "tray.pause": "Tạm dừng phím tắt", "tray.resume": "Tiếp tục phím tắt", @@ -43,7 +40,6 @@ "close.tray": "Giữ trong khay", "close.quit": "Thoát", "close.dont_ask": "Không hỏi lại", - "home.title": "Trang chủ", "home.drop_title": "Thả tệp vào đây", "home.drop_select": "Nhấp đúp để chọn tệp", @@ -72,7 +68,6 @@ "home.undone": "Đã hoàn tác: kết quả đã ở trong Thùng rác", "home.undone_original": "Đã hoàn tác: kết quả đã ở trong Thùng rác, cùng với các bản gốc mà chúng đã thay thế", "home.undo_failed": "Không thể chuyển {0} vào Thùng rác: {1}", - "donut.center_drag": "Đưa tệp vào đây", "donut.files_same": "{0} × {1}", "donut.files_mixed": "{0} tệp", @@ -88,7 +83,6 @@ "donut.group_caption": "Nhóm {0} mục", "donut.flip_to_back": "{0} mặt sau", "donut.flip_to_front": "{0} mặt trước", - "toast.converting": "{0} → {1}", "toast.done": "Xong", "toast.done_errors": "{0}/{1} tệp lỗi", @@ -101,7 +95,6 @@ "toast.get_fonts": "Tải phông chữ", "download.badge": "{0}/s · {1}", "download.badge_files": "{0} · {1}/{2} tệp", - "error.unsupported_source": "Loại tệp này không được hỗ trợ", "error.empty_file": "Tệp trống (0 byte). Tệp tạo bằng “Mới” trong menu chuột phải sẽ trống cho đến khi bạn mở nó trong ứng dụng tương ứng và lưu lại", "error.p7m_unreadable": "Đây không phải tệp .p7m đã ký mà Filee mở được, hoặc tệp bị hỏng", @@ -194,7 +187,6 @@ "notice.geo_field_names": "Tên trường Shapefile dài tối đa 10 ký tự: một số tên đã được rút ngắn", "notice.geo_shapefile_split": "Shapefile chỉ chứa một loại hình, nên điểm, đường và đa giác đã được lưu thành các tệp riêng", "notice.geo_heights_dropped": "Một số điểm không có độ cao mà Shapefile không thể bỏ trống, nên mọi độ cao đã bị bỏ qua", - "engine.reason.not_installed": "Chưa cài đặt", "engine.reason.error": "Lỗi khi kiểm tra", "engine.reason.unsupported_platform": "Không có trên nền tảng này", @@ -257,7 +249,6 @@ "engine.calibre.description": "Định dạng sách điện tử hiếm (LIT, LRF, CHM, PDB, RB…) và đầu ra Kindle (MOBI, AZW3).\nKhi đặt trên trình ghi HWPX, Calibre cũng tạo PDF cho sách điện tử (nhanh hơn nhiều với sách dài).", "engine.libreoffice.description": "Đọc các định dạng cũ hơn và ít phổ biến: DOC, PPT, Pages, Keynote, Numbers, WordPerfect, ClarisWorks, Publisher, Visio…\nCũng lưu dưới dạng DOC, XLS, PPT, ODT, ODP, mẫu OpenDocument và EMF.", "engine.translator.description": "Dịch tài liệu và sách điện tử ngay trên máy bằng các mô hình Ollama ngoại tuyến.\nCũng dịch phụ đề SRT và WebVTT: chỉ văn bản của từng câu phụ đề được gửi tới mô hình, số thứ tự và mốc thời gian được giữ nguyên.", - "profile.images": "Ảnh", "profile.pdf": "PDF", "profile.office": "Tài liệu", @@ -277,7 +268,6 @@ "profile.models": "Mô hình 3D", "profile.geo": "Không gian địa lý", "profile.mixed": "Tệp hỗn hợp", - "preset.crop": "Cắt", "crop.save": "Cắt & lưu", "crop.reset": "Đặt lại", @@ -404,7 +394,6 @@ "clip.silence_removed": "Đã gỡ đoạn im lặng: {0}", "clip.no_silence": "Không tìm thấy đoạn im lặng", "clip.cannot_play": "Không thể phát đoạn này", - "preset.half_size": "Nửa kích thước", "preset.half_size.donut": "Nửa kích thước", "preset.image_edit": "Chỉnh sửa ảnh", @@ -519,7 +508,6 @@ "preset.merge_audio": "Ghép âm thanh", "preset.merge_images": "Ghép ảnh", "preset.remove_metadata": "Xóa siêu dữ liệu", - "format.folder": "Thư mục", "format.category.Image": "Ảnh", "format.category.Pdf": "PDF", @@ -539,7 +527,6 @@ "format.category.Data": "Dữ liệu", "format.category.Model3D": "Mô hình 3D", "format.category.Geo": "Không gian địa lý", - "general.title": "Chung", "general.language": "Ngôn ngữ", "general.language_auto": "Mặc định của hệ thống", @@ -622,7 +609,6 @@ "general.reset_library": "Đặt lại mặc định", "general.reset_confirm": "Thay thế mọi cấu hình sẵn và hồ sơ thanh công cụ bằng mặc định?", "general.imported": "Đã nhập {0} mục", - "triggers.title": "Phím tắt", "triggers.description": "Chọn cách mở thanh công cụ vòng tròn. Bạn có thể thêm bao nhiêu thao tác tùy thích.", "triggers.macos_permissions": "Cho phép Filee trong Cài đặt hệ thống > Quyền riêng tư & bảo mật > Trợ năng.\nNếu Filee đã được bật nhưng thao tác không hoạt động, hãy gỡ bỏ bằng − rồi thêm lại bằng +.\nFinder cũng hỏi quyền Tự động hóa ở lần đầu bạn dùng phím tắt cho vùng chọn.", @@ -680,7 +666,6 @@ "triggers.excluded_none_running": "Không có ứng dụng nào khác đang mở cửa sổ.", "triggers.excluded_note": "Thao tác và phím tắt không có tác dụng khi con trỏ nằm trên các ứng dụng này.", "triggers.paused": "Phím tắt đang tạm dừng", - "toolbar.title": "Thanh công cụ vòng tròn", "toolbar.step_profile": "1. Chọn một hồ sơ", "toolbar.step_arrange": "2. Sắp xếp cấu hình sẵn", @@ -748,18 +733,17 @@ "toolbar.mixed_covered": "“{0}” đứng trước và cho phép mọi phần mở rộng được chọn ở đây, nên hồ sơ này không bao giờ được dùng.", "toolbar.mixed_others": "Các tổ hợp tệp khác sẽ mở “{0}”.", "toolbar.mixed_others_self": "Không còn hồ sơ hỗn hợp nào không chọn mục nào, nên hồ sơ này cũng được dùng cho mọi tổ hợp khác.", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ Cùng định dạng", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "Định dạng được nhận: {0}", - "toolbar.chip_tip.kind": "{0} ({1})", - "toolbar.chip_tip.name_separator": ", ", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} mục khác · Shift để hiện tất cả", - "toolbar.chip_tip.shift_all": "Shift để hiện tất cả", - "toolbar.chip_tip.any_file": "Mọi tệp", - "tags.remove": "Gỡ bỏ", - + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ Cùng định dạng", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "Định dạng được nhận: {0}", + "toolbar.chip_tip.kind": "{0} ({1})", + "toolbar.chip_tip.name_separator": ", ", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} mục khác · Shift để hiện tất cả", + "toolbar.chip_tip.shift_all": "Shift để hiện tất cả", + "toolbar.chip_tip.any_file": "Mọi tệp", + "tags.remove": "Gỡ bỏ", "presets.title": "Cấu hình sẵn", "presets.add": "Mới", "presets.add_empty": "Cấu hình sẵn trống", @@ -1083,7 +1067,6 @@ "presets.delete_confirm": "Xóa “{0}”? Cấu hình sẵn này cũng sẽ bị gỡ khỏi mọi thanh công cụ và nhóm.", "presets.name_required": "Nhập tên", "presets.select": "Chọn một cấu hình sẵn để chỉnh sửa", - "theme.title": "Chủ đề", "theme.style": "Kiểu chủ đề", "theme.style.Soft": "Filee Soft", @@ -1120,7 +1103,6 @@ "theme.preview": "Xem trước", "theme.preview_toggle": "Công tắc mẫu (chỉ để xem trước, không phải cài đặt)", "theme.preview_toggle_hint": "Cho thấy công tắc trông thế nào trong chủ đề này. Không thay đổi cài đặt nào.", - "engines.title": "Bộ xử lý chuyển đổi", "engines.description": "Filee chọn bộ xử lý có sẵn tốt nhất cho mỗi lần chuyển đổi.\nKhi nhiều bộ xử lý làm được cùng một việc, bộ xử lý ở trên sẽ được dùng.", "engines.available": "Sẵn sàng", @@ -1197,7 +1179,6 @@ "engines.state.unpacking": "Đang cài đặt…", "engines.state.installed": "Đã cài đặt", "engines.state.failed": "Thất bại: {0}", - "about.title": "Giới thiệu", "about.updates": "Cập nhật", "common.search": "Tìm kiếm", @@ -1222,7 +1203,6 @@ "feedback.notice_text": "Một ngôi sao trên GitHub giúp người khác tìm thấy Filee.\nVấn đề và ý tưởng cũng luôn được hoan nghênh.", "feedback.star": "Gắn sao trên GitHub", "feedback.issue": "Báo cáo vấn đề hoặc ý tưởng", - "common.ok": "OK", "common.cancel": "Hủy", "common.save": "Lưu", @@ -1232,7 +1212,6 @@ "common.no": "Không", "common.on": "Bật", "common.off": "Tắt", - "setup.title": "Chọn bộ xử lý chuyển đổi", "setup.description": "Ảnh, PDF, tài liệu Office, HWP, sách điện tử, tệp nén, phông chữ, CAD và mô hình 3D đã chuyển đổi được.\nChỉ tải xuống bộ xử lý cho video và âm thanh hoặc các định dạng hiếm.", "setup.note": "Bạn có thể cài đặt hoặc gỡ bỏ bộ xử lý bất cứ lúc nào trong Cài đặt → Bộ xử lý.", @@ -1395,7 +1374,6 @@ "watch.status_starting": "Đang khởi động…", "watch.status_error": "Không thể theo dõi thư mục này: {0}", "watch.cli_hint": "Cũng có trên dòng lệnh: filee watch --to ", - "preset.sheet_studio": "PDF + chỉnh sửa", "sheet_studio.title": "Xưởng in bảng tính", "sheet_studio.sheets": "Trang tính và thứ tự đầu ra", @@ -1435,7 +1413,6 @@ "sheet_studio.apply_fix": "Áp dụng bố cục đề xuất", "sheet_studio.add_region": "+ Thêm vùng khác", "sheet_studio.region_name": "{0} · vùng {1}", - "whats_new.title": "Có gì mới trong Filee {0}", "whats_new.previous": "Trước", "whats_new.next": "Tiếp", @@ -1513,6 +1490,28 @@ "engines.ollama.complete": "Hoàn tất!", "engines.ollama.failed": "Thất bại:", "engines.ollama.error": "Lỗi:", + "preset.upscale": "Nâng cao chất lượng ảnh", + "preset.upscale.donut": "Nâng độ nét", + "engine.upscaler.name": "Mô hình chất lượng hình ảnh", + "engine.upscaler.description": "Nâng độ phân giải ảnh AI lên tới 4× với mô hình NCNN cục bộ.", + "engines.upscaler.modal_description": "Tải các mô hình siêu độ phân giải AI cục bộ để phóng to hình ảnh lên tới 4×. Toàn bộ xử lý chạy 100% ngoại tuyến trên thiết bị của bạn.", + "upscale.model": "Mô hình", + "upscale.scale": "Hệ số tỷ lệ", + "upscale.output_format": "Định dạng đầu ra", + "upscale.format_same": "Cùng định dạng với nguồn ({0})", + "upscale.resolution_label": "Xem trước độ phân giải", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "Bắt đầu nâng chất lượng", + "upscale.success": "Nâng chất lượng ảnh hoàn tất thành công.", + "error.upscale_failed": "Nâng chất lượng ảnh thất bại: {0}", + "engines.package.upscayl-bin.name": "Upscayl (Bộ nâng độ nét ảnh AI)", + "engines.package.upscayl-bin.description": "Nâng độ phân giải ảnh AI lên tới 4× với mô hình NCNN cục bộ.\nMô hình sẽ được tải xuống trong lần đầu tiên sử dụng.", + "error.upscale_needs_engine": "Cài đặt Upscayl trong Cài đặt → Bộ xử lý để sử dụng nâng cấp hình ảnh AI.", + "error.upscale_needs_vulkan": "Nâng cấp độ phân giải yêu cầu cạc đồ họa (GPU) tương thích với Vulkan.", + "upscale.model_ready": "Sẵn sàng", + "upscale.model_download_size": "Tải xuống {0} MB", + "upscale.downloading_model": "Đang tải mô hình...", + "engines.model.delete_confirm": "Xóa mô hình {0}? Bạn có thể tải lại bất kỳ lúc nào.", "home.queue_error": "Không thể chuyển đổi {0}. Danh sách vẫn giữ các tệp.", "home.queue_selected": "đã chọn", "home.queue_remove_file": "Bỏ {0}", diff --git a/src/Filee.App/Assets/i18n/zh-CN.json b/src/Filee.App/Assets/i18n/zh-CN.json index fcfb1ecd..7b8a426d 100644 --- a/src/Filee.App/Assets/i18n/zh-CN.json +++ b/src/Filee.App/Assets/i18n/zh-CN.json @@ -1,7 +1,6 @@ { "app.name": "Filee", "app.tagline": "拖一拖,放一放,转换完成。", - "fields.title": "领域", "fields.description": "关闭您不使用的领域。仅属于已关闭领域的格式不会出现在圆环的扇区、“设置 → 环形工具栏”中的“可用预设”、“设置 → 预设”中的列表以及预设的“转换为”列表中。Filee 仍会识别这些格式的文件,命令行仍可转换为这些格式;拖入的文件也仍会提供其所属领域的格式。", "fields.formats": "格式:{0}", @@ -18,7 +17,6 @@ "fields.name.video-pro": "视频制作与广播", "fields.name.audio-pro": "专业音频", "fields.name.legacy": "旧格式", - "nav.home": "首页", "nav.general": "常规", "nav.triggers": "快捷操作", @@ -31,7 +29,6 @@ "nav.about": "关于", "nav.collapse": "收起侧边栏", "nav.expand": "展开侧边栏", - "tray.open": "打开 Filee", "tray.pause": "暂停快捷操作", "tray.resume": "恢复快捷操作", @@ -43,7 +40,6 @@ "close.tray": "保留在托盘", "close.quit": "退出", "close.dont_ask": "不再询问", - "home.title": "首页", "home.drop_title": "将文件拖放到这里", "home.drop_select": "双击选择文件", @@ -72,7 +68,6 @@ "home.undone": "已撤销:结果已在回收站/废纸篓中", "home.undone_original": "已撤销:结果已在回收站/废纸篓中,被替换的原文件也在那里", "home.undo_failed": "无法将 {0} 移到回收站/废纸篓:{1}", - "donut.center_drag": "把文件拖到这里", "donut.files_same": "{0} 个 {1}", "donut.files_mixed": "{0} 个文件", @@ -88,7 +83,6 @@ "donut.group_caption": "组 · {0} 项", "donut.flip_to_back": "{0} 背面", "donut.flip_to_front": "{0} 正面", - "toast.converting": "{0} → {1}", "toast.done": "完成", "toast.done_errors": "{1} 个中有 {0} 个失败", @@ -101,7 +95,6 @@ "toast.get_fonts": "获取字体", "download.badge": "{0}/s · {1}", "download.badge_files": "{0} · 第 {1}/{2} 个文件", - "error.unsupported_source": "不支持此文件类型", "error.empty_file": "文件是空的(0 字节)。用右键菜单“新建”创建的文件,要在对应的应用中打开并保存后才有内容", "error.p7m_unreadable": "这不是 Filee 能打开的已签名 .p7m 文件,或文件已损坏", @@ -194,7 +187,6 @@ "notice.geo_field_names": "Shapefile 字段名最多 10 个字符,部分名称已缩短", "notice.geo_shapefile_split": "Shapefile 只能保存一种形状,因此点、线、面被分别存成了多个文件", "notice.geo_heights_dropped": "部分点没有高度,而 Shapefile 无法只省略其中一个点的高度,因此已省略所有高度", - "engine.reason.not_installed": "未安装", "engine.reason.error": "检查时出错", "engine.reason.unsupported_platform": "此平台不可用", @@ -257,7 +249,6 @@ "engine.calibre.description": "少见的电子书格式(LIT、LRF、CHM、PDB、RB…)以及 Kindle 输出(MOBI、AZW3)。\n排在 HWPX writer 之上时,也由它把电子书转为 PDF(长书快得多)。", "engine.libreoffice.description": "读取较旧或少见的格式,如 DOC、PPT、Pages、Keynote、Numbers、WordPerfect、ClarisWorks、Publisher、Visio。\n也能保存为 DOC、XLS、PPT、ODT、ODP、OpenDocument 模板或 EMF。", "engine.translator.description": "使用离线 Ollama 模型在本地翻译文档和电子书。\n也能翻译 SRT 和 WebVTT 字幕:只把字幕文本发送给模型,字幕序号和时间轴保持不变。", - "profile.images": "图片", "profile.pdf": "PDF", "profile.office": "文档", @@ -277,7 +268,6 @@ "profile.models": "3D 模型", "profile.geo": "地理信息 (GIS)", "profile.mixed": "混合文件", - "preset.crop": "裁剪", "crop.save": "裁剪并保存", "crop.reset": "重置", @@ -404,7 +394,6 @@ "clip.silence_removed": "已去除静音:{0} 处", "clip.no_silence": "没有找到静音", "clip.cannot_play": "无法播放片段", - "preset.half_size": "缩小 50%", "preset.half_size.donut": "缩小 50%", "preset.image_edit": "编辑图片", @@ -519,7 +508,6 @@ "preset.merge_audio": "合并音频", "preset.merge_images": "合并图片", "preset.remove_metadata": "移除元数据", - "format.folder": "文件夹", "format.category.Image": "图片", "format.category.Pdf": "PDF", @@ -539,7 +527,6 @@ "format.category.Data": "数据", "format.category.Model3D": "3D 模型", "format.category.Geo": "地理信息", - "general.title": "常规", "general.language": "语言", "general.language_auto": "跟随系统", @@ -622,7 +609,6 @@ "general.reset_library": "恢复默认", "general.reset_confirm": "要将所有预设和工具栏配置恢复为默认吗?", "general.imported": "已导入 {0} 项", - "triggers.title": "快捷操作", "triggers.description": "选择打开环形工具栏的方式。可以添加任意多个手势。", "triggers.macos_permissions": "请在系统设置 > 隐私与安全性 > 辅助功能中允许 Filee。\n如果 Filee 已打开但手势仍未启动,请用 − 将其移除,再用 + 重新添加。\n首次使用选择文件的快捷键时,Finder 还会请求自动化权限。", @@ -680,7 +666,6 @@ "triggers.excluded_none_running": "没有其他打开窗口的应用。", "triggers.excluded_note": "指针位于这些应用上时,手势和快捷键不起作用。", "triggers.paused": "快捷操作已暂停", - "toolbar.title": "环形工具栏", "toolbar.step_profile": "1. 选择配置", "toolbar.step_arrange": "2. 排列预设", @@ -748,18 +733,17 @@ "toolbar.mixed_covered": "靠前的“{0}”允许此处勾选的所有扩展名,因此不会使用此配置。", "toolbar.mixed_others": "其他混合选择会打开“{0}”。", "toolbar.mixed_others_self": "没有未勾选任何项的混合配置,因此其他混合选择也会使用此配置。", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ 相同格式", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "支持的格式:{0}", - "toolbar.chip_tip.kind": "{0}({1})", - "toolbar.chip_tip.name_separator": "、", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} 项 · 按 Shift 显示全部", - "toolbar.chip_tip.shift_all": "按 Shift 显示全部", - "toolbar.chip_tip.any_file": "任意文件", - "tags.remove": "移除", - + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ 相同格式", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "支持的格式:{0}", + "toolbar.chip_tip.kind": "{0}({1})", + "toolbar.chip_tip.name_separator": "、", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} 项 · 按 Shift 显示全部", + "toolbar.chip_tip.shift_all": "按 Shift 显示全部", + "toolbar.chip_tip.any_file": "任意文件", + "tags.remove": "移除", "presets.title": "预设", "presets.add": "新建", "presets.add_empty": "空白预设", @@ -1083,7 +1067,6 @@ "presets.delete_confirm": "删除“{0}”?它也会从所有工具栏和组中移除。", "presets.name_required": "请输入名称", "presets.select": "选择要编辑的预设", - "theme.title": "主题", "theme.style": "主题样式", "theme.style.Soft": "Filee Soft", @@ -1120,7 +1103,6 @@ "theme.preview": "预览", "theme.preview_toggle": "示例开关(仅供预览,不是设置)", "theme.preview_toggle_hint": "展示此主题中开关的外观,不会更改任何设置。", - "engines.title": "转换引擎", "engines.description": "Filee 会为每次转换选择最合适的可用引擎。\n多个引擎能完成同一转换时,列表中靠前的优先。", "engines.available": "可用", @@ -1197,7 +1179,6 @@ "engines.state.unpacking": "安装中…", "engines.state.installed": "已安装", "engines.state.failed": "失败:{0}", - "about.title": "关于", "about.updates": "更新", "common.search": "搜索", @@ -1222,7 +1203,6 @@ "feedback.notice_text": "在 GitHub 上点个星(⭐)能帮助更多人发现 Filee。\n也欢迎反馈问题或提出想法。", "feedback.star": "在 GitHub 上点星", "feedback.issue": "报告问题 · 提出建议", - "common.ok": "确定", "common.cancel": "取消", "common.save": "保存", @@ -1232,7 +1212,6 @@ "common.no": "否", "common.on": "开", "common.off": "关", - "setup.title": "选择转换引擎", "setup.description": "图片、PDF、Office 文档、HWP、电子书、压缩文件、字体、CAD 和 3D 模型现在就能转换。\n只有视频、音频或少见格式才需要下载引擎。", "setup.note": "可随时在“设置 → 转换引擎”中安装或删除引擎。", @@ -1395,7 +1374,6 @@ "watch.status_starting": "正在启动…", "watch.status_error": "无法监视此文件夹:{0}", "watch.cli_hint": "也可以在命令行中使用:filee watch <文件夹> --to <格式>", - "preset.sheet_studio": "PDF + 编辑", "sheet_studio.title": "电子表格打印工作室", "sheet_studio.sheets": "工作表和输出顺序", @@ -1435,7 +1413,6 @@ "sheet_studio.apply_fix": "应用建议布局", "sheet_studio.add_region": "+ 添加另一个区域", "sheet_studio.region_name": "{0} · 区域 {1}", - "whats_new.title": "Filee {0} 新功能", "whats_new.previous": "上一页", "whats_new.next": "下一页", @@ -1513,6 +1490,28 @@ "engines.ollama.complete": "完成!", "engines.ollama.failed": "失败:", "engines.ollama.error": "错误:", + "preset.upscale": "放大图像画质", + "preset.upscale.donut": "超分辨率放大", + "engine.upscaler.name": "画质提升模型", + "engine.upscaler.description": "使用本地 NCNN 模型实现最高 4 倍的 AI 图像超分辨率高保真放大。", + "engines.upscaler.modal_description": "下载本地 AI 超分辨率模型,将图像最高放大 4 倍。所有处理完全在本地离线进行,不消耗网络流量。", + "upscale.model": "模型", + "upscale.scale": "放大倍率", + "upscale.output_format": "输出格式", + "upscale.format_same": "与源格式相同 ({0})", + "upscale.resolution_label": "分辨率预览", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "立即放大", + "upscale.success": "画质提升放大已完成。", + "error.upscale_failed": "画质提升失败:{0}", + "engines.package.upscayl-bin.name": "Upscayl(AI 图像超分辨率放大)", + "engines.package.upscayl-bin.description": "使用本地 NCNN 模型实现最高 4 倍的 AI 图像放大与超分辨率。\n模型在首次使用时下载。", + "error.upscale_needs_engine": "请在“设置 → 转换引擎”中安装 Upscayl 以使用 AI 放大功能。", + "error.upscale_needs_vulkan": "放大图像需要支持 Vulkan 的显卡 (GPU)。", + "upscale.model_ready": "就绪", + "upscale.model_download_size": "{0} MB 下载", + "upscale.downloading_model": "正在下载模型...", + "engines.model.delete_confirm": "是否删除模型 {0}?您可以随时重新下载。", "home.queue_error": "无法转换 {0}。列表保留这些文件。", "home.queue_selected": "已选中", "home.queue_remove_file": "移除 {0}", diff --git a/src/Filee.App/Assets/i18n/zh-TW.json b/src/Filee.App/Assets/i18n/zh-TW.json index 92a8f1ad..98160f39 100644 --- a/src/Filee.App/Assets/i18n/zh-TW.json +++ b/src/Filee.App/Assets/i18n/zh-TW.json @@ -1,7 +1,6 @@ { "app.name": "Filee", "app.tagline": "拖一拖,放一放,轉換完成。", - "fields.title": "領域", "fields.description": "關閉您不使用的領域。僅屬於已關閉領域的格式不會出現在圓環的扇形、「設定 → 環形工具列」中的「可用預設」、「設定 → 預設」中的清單以及預設的「轉換為」清單中。Filee 仍會辨識這些格式的檔案,命令列仍可轉換為這些格式;拖入的檔案也仍會提供其所屬領域的格式。", "fields.formats": "格式:{0}", @@ -18,7 +17,6 @@ "fields.name.video-pro": "影片製作與廣播", "fields.name.audio-pro": "專業音訊", "fields.name.legacy": "舊格式", - "nav.home": "首頁", "nav.general": "一般", "nav.triggers": "觸發方式", @@ -31,7 +29,6 @@ "nav.about": "關於", "nav.collapse": "收合側邊欄", "nav.expand": "展開側邊欄", - "tray.open": "開啟 Filee", "tray.pause": "暫停快捷操作", "tray.resume": "恢復快捷操作", @@ -43,7 +40,6 @@ "close.tray": "保留在系統匣", "close.quit": "結束", "close.dont_ask": "不再詢問", - "home.title": "首頁", "home.drop_title": "將檔案拖放到這裡", "home.drop_select": "按兩下選取檔案", @@ -72,7 +68,6 @@ "home.undone": "已復原:結果已在資源回收筒/垃圾桶中", "home.undone_original": "已復原:結果已在資源回收筒/垃圾桶中,被取代的原始檔也在那裡", "home.undo_failed": "無法將 {0} 移到資源回收筒/垃圾桶:{1}", - "donut.center_drag": "把檔案拖到這裡", "donut.files_same": "{0} 個 {1}", "donut.files_mixed": "{0} 個檔案", @@ -88,7 +83,6 @@ "donut.group_caption": "群組 · {0} 個項目", "donut.flip_to_back": "{0} 背面", "donut.flip_to_front": "{0} 正面", - "toast.converting": "{0} → {1}", "toast.done": "完成", "toast.done_errors": "{1} 個中有 {0} 個失敗", @@ -101,7 +95,6 @@ "toast.get_fonts": "取得字型", "download.badge": "{0}/s · {1}", "download.badge_files": "{0} · 第 {1}/{2} 個檔案", - "error.unsupported_source": "不支援此檔案類型", "error.empty_file": "檔案是空的(0 位元組)。用右鍵選單「新增」建立的檔案,要在對應的應用程式中開啟並儲存後才有內容", "error.p7m_unreadable": "這不是 Filee 能開啟的已簽署 .p7m 檔案,或檔案已損壞", @@ -194,7 +187,6 @@ "notice.geo_field_names": "Shapefile 欄位名最多 10 個字元,部分名稱已縮短", "notice.geo_shapefile_split": "Shapefile 只能儲存一種形狀,因此點、線、面被分別存成了多個檔案", "notice.geo_heights_dropped": "部分點沒有高度,而 Shapefile 無法只省略其中一個點的高度,因此已省略所有高度", - "engine.reason.not_installed": "未安裝", "engine.reason.error": "檢查時出錯", "engine.reason.unsupported_platform": "此平台不可用", @@ -257,7 +249,6 @@ "engine.calibre.description": "少見的電子書格式(LIT、LRF、CHM、PDB、RB…)以及 Kindle 輸出(MOBI、AZW3)。\n排在 HWPX writer 之上時,也由它把電子書轉為 PDF(長書快得多)。", "engine.libreoffice.description": "讀取較舊或少見的格式,如 DOC、PPT、Pages、Keynote、Numbers、WordPerfect、ClarisWorks、Publisher、Visio。\n也能儲存為 DOC、XLS、PPT、ODT、ODP、OpenDocument 範本或 EMF。", "engine.translator.description": "使用離線 Ollama 模型在本地翻譯文件與電子書。\n也能翻譯 SRT 和 WebVTT 字幕:只將字幕文字傳送給模型,字幕序號和時間軸保持不變。", - "profile.images": "圖片", "profile.pdf": "PDF", "profile.office": "文件", @@ -277,7 +268,6 @@ "profile.models": "3D 模型", "profile.geo": "地理資訊 (GIS)", "profile.mixed": "混合檔案", - "preset.crop": "裁剪", "crop.save": "裁剪並儲存", "crop.reset": "重設", @@ -404,7 +394,6 @@ "clip.silence_removed": "已去除靜音:{0} 處", "clip.no_silence": "沒有找到靜音", "clip.cannot_play": "無法播放片段", - "preset.half_size": "縮小 50%", "preset.half_size.donut": "縮小 50%", "preset.image_edit": "編輯圖片", @@ -519,7 +508,6 @@ "preset.merge_audio": "合併音訊", "preset.merge_images": "合併圖片", "preset.remove_metadata": "移除中繼資料", - "format.folder": "資料夾", "format.category.Image": "圖片", "format.category.Pdf": "PDF", @@ -539,7 +527,6 @@ "format.category.Data": "資料", "format.category.Model3D": "3D 模型", "format.category.Geo": "地理資訊", - "general.title": "一般", "general.language": "語言", "general.language_auto": "跟隨系統", @@ -622,7 +609,6 @@ "general.reset_library": "還原為初始設定", "general.reset_confirm": "要將所有預設和工具列設定檔還原為初始設定嗎?", "general.imported": "已匯入 {0} 項", - "triggers.title": "觸發方式", "triggers.description": "選擇開啟環形工具列的方式。可以新增任意多個手勢。", "triggers.macos_permissions": "請在「系統設定 > 隱私權與安全性 > 輔助使用」中允許 Filee。\n如果 Filee 已開啟但手勢仍未啟動,請用 − 將其移除,再用 + 重新加入。\n首次使用「選取檔案」的快速鍵時,Finder 還會要求自動化權限。", @@ -680,7 +666,6 @@ "triggers.excluded_none_running": "沒有其他開啟視窗的應用程式。", "triggers.excluded_note": "指標位於這些應用程式上時,手勢和快速鍵不起作用。", "triggers.paused": "快捷操作已暫停", - "toolbar.title": "環形工具列", "toolbar.step_profile": "1. 選擇設定檔", "toolbar.step_arrange": "2. 排列預設", @@ -748,18 +733,17 @@ "toolbar.mixed_covered": "排在前面的「{0}」允許此處勾選的所有副檔名,因此不會使用此設定檔。", "toolbar.mixed_others": "其他混合選取會開啟「{0}」。", "toolbar.mixed_others_self": "沒有未勾選任何項目的混合設定檔,因此其他混合選取也會使用此設定檔。", - "toolbar.chip_tip.makes": "→ {0} · {1}", - "toolbar.chip_tip.makes_same": "→ 相同格式", - "toolbar.chip_tip.makes_name": "→ {0}", - "toolbar.chip_tip.works_with": "支援的格式:{0}", - "toolbar.chip_tip.kind": "{0}({1})", - "toolbar.chip_tip.name_separator": "、", - "toolbar.chip_tip.names_more": "{0} +{1}", - "toolbar.chip_tip.more_shift": "+{0} 項 · 按 Shift 顯示全部", - "toolbar.chip_tip.shift_all": "按 Shift 顯示全部", - "toolbar.chip_tip.any_file": "任何檔案", - "tags.remove": "移除", - + "toolbar.chip_tip.makes": "→ {0} · {1}", + "toolbar.chip_tip.makes_same": "→ 相同格式", + "toolbar.chip_tip.makes_name": "→ {0}", + "toolbar.chip_tip.works_with": "支援的格式:{0}", + "toolbar.chip_tip.kind": "{0}({1})", + "toolbar.chip_tip.name_separator": "、", + "toolbar.chip_tip.names_more": "{0} +{1}", + "toolbar.chip_tip.more_shift": "+{0} 項 · 按 Shift 顯示全部", + "toolbar.chip_tip.shift_all": "按 Shift 顯示全部", + "toolbar.chip_tip.any_file": "任何檔案", + "tags.remove": "移除", "presets.title": "預設", "presets.add": "新增", "presets.add_empty": "空白預設", @@ -1083,7 +1067,6 @@ "presets.delete_confirm": "刪除「{0}」?它也會從所有工具列和群組中移除。", "presets.name_required": "請輸入名稱", "presets.select": "選擇要編輯的預設", - "theme.title": "主題", "theme.style": "主題樣式", "theme.style.Soft": "Filee Soft", @@ -1120,7 +1103,6 @@ "theme.preview": "預覽", "theme.preview_toggle": "範例開關(僅供預覽,不是設定)", "theme.preview_toggle_hint": "顯示此主題中開關的外觀,不會變更任何設定。", - "engines.title": "轉換引擎", "engines.description": "Filee 會為每次轉換選擇最合適的可用引擎。\n多個引擎能完成同一轉換時,清單中排在前面的優先。", "engines.available": "可用", @@ -1197,7 +1179,6 @@ "engines.state.unpacking": "安裝中…", "engines.state.installed": "已安裝", "engines.state.failed": "失敗:{0}", - "about.title": "關於", "about.updates": "更新", "common.search": "搜尋", @@ -1222,7 +1203,6 @@ "feedback.notice_text": "在 GitHub 上加個星號(⭐)能幫助更多人發現 Filee。\n也歡迎回報問題或提出想法。", "feedback.star": "在 GitHub 上加星號", "feedback.issue": "回報問題 · 提出建議", - "common.ok": "確定", "common.cancel": "取消", "common.save": "儲存", @@ -1232,7 +1212,6 @@ "common.no": "否", "common.on": "開", "common.off": "關", - "setup.title": "選擇轉換引擎", "setup.description": "圖片、PDF、Office 文件、HWP、電子書、壓縮檔、字型、CAD 和 3D 模型現在就能轉換。\n只有影片、音訊或少見格式才需要下載引擎。", "setup.note": "可隨時在「設定 → 轉換引擎」中安裝或刪除引擎。", @@ -1395,7 +1374,6 @@ "watch.status_starting": "正在啟動…", "watch.status_error": "無法監視此資料夾:{0}", "watch.cli_hint": "也可以在命令列中使用:filee watch <資料夾> --to <格式>", - "preset.sheet_studio": "PDF + 編輯", "sheet_studio.title": "試算表列印工作室", "sheet_studio.sheets": "工作表與輸出順序", @@ -1435,7 +1413,6 @@ "sheet_studio.apply_fix": "套用建議版面配置", "sheet_studio.add_region": "+ 新增另一個範圍", "sheet_studio.region_name": "{0} · 範圍 {1}", - "whats_new.title": "Filee {0} 新功能", "whats_new.previous": "上一頁", "whats_new.next": "下一頁", @@ -1513,6 +1490,28 @@ "engines.ollama.complete": "完成!", "engines.ollama.failed": "失敗:", "engines.ollama.error": "錯誤:", + "preset.upscale": "放大圖像畫質", + "preset.upscale.donut": "超解析度放大", + "engine.upscaler.name": "畫質提升模型", + "engine.upscaler.description": "使用本機 NCNN 模型實現最高 4 倍的 AI 圖像超解析度高保真放大。", + "engines.upscaler.modal_description": "下載本機 AI 超解析度模型,將圖像最高放大 4 倍。所有處理完全在本機離線進行,確保隱私安全。", + "upscale.model": "模型", + "upscale.scale": "放大倍率", + "upscale.output_format": "輸出格式", + "upscale.format_same": "與原格式相同 ({0})", + "upscale.resolution_label": "解析度預覽", + "upscale.resolution_preview": "{0} × {1} px ➜ {2} × {3} px ({4}×)", + "upscale.button_start": "立即放大", + "upscale.success": "畫質提升放大已完成。", + "error.upscale_failed": "畫質提升失敗:{0}", + "engines.package.upscayl-bin.name": "Upscayl(AI 影像超解析度放大)", + "engines.package.upscayl-bin.description": "使用本機 NCNN 模型提供最高 4 倍的 AI 影像放大與超解析度。\n模型在第一次使用時下載。", + "error.upscale_needs_engine": "請在「設定 → 轉換引擎」中安裝 Upscayl 以使用 AI 放大功能。", + "error.upscale_needs_vulkan": "放大影像需要支援 Vulkan 的顯示卡 (GPU)。", + "upscale.model_ready": "就緒", + "upscale.model_download_size": "{0} MB 下載", + "upscale.downloading_model": "正在下載模型...", + "engines.model.delete_confirm": "確定要刪除模型 {0} 嗎?您可以隨時重新下載。", "home.queue_error": "無法轉換 {0}。清單會保留這些檔案。", "home.queue_selected": "已選取", "home.queue_remove_file": "移除 {0}", diff --git a/src/Filee.App/Services/PresetAvailability.cs b/src/Filee.App/Services/PresetAvailability.cs index 32d17b14..84ff6d15 100644 --- a/src/Filee.App/Services/PresetAvailability.cs +++ b/src/Filee.App/Services/PresetAvailability.cs @@ -145,6 +145,10 @@ private PresetJudgement Evaluate(Preset preset, IReadOnlyList format return formats.Any(f => HideService.CanOpen(f.Id)) ? new(PresetUse.Usable, null) : new(PresetUse.Unusable, _loc["error.images_only"]); + if (preset.IsUpscaleTool) + return formats.Any(f => UpscaleTool.CanUpscale(f.Id)) + ? new(PresetUse.Usable, null) + : new(PresetUse.Unusable, _loc["error.images_only"]); if (preset.IsTool) { diff --git a/src/Filee.App/Services/WindowService.cs b/src/Filee.App/Services/WindowService.cs index 970e9ff4..e6bd957a 100644 --- a/src/Filee.App/Services/WindowService.cs +++ b/src/Filee.App/Services/WindowService.cs @@ -40,12 +40,14 @@ public sealed class WindowService(ILocalizer loc, IServiceProvider services, Mem private AnnotateWindow? _annotateWindow; private BackgroundWindow? _backgroundWindow; private ImageEditWindow? _imageEdit; + private UpscaleWindow? _upscaleWindow; internal ClipWindow? ClipWindow => _clipWindow; internal HideWindow? HideWindow => _hideWindow; internal AnnotateWindow? AnnotateWindow => _annotateWindow; internal BackgroundWindow? BackgroundWindow => _backgroundWindow; internal ImageEditWindow? ImageEdit => _imageEdit; + internal UpscaleWindow? UpscaleWindow => _upscaleWindow; private readonly HashSet _spreadsheetStudios = []; internal IReadOnlyCollection SpreadsheetStudios => _spreadsheetStudios; @@ -208,6 +210,8 @@ public void ShowTool(IReadOnlyList files, Preset preset, PixelPoint? nea ShowBackgroundWindow(files, preset); else if (preset.IsImageEditTool) ShowImageEditWindow(files, preset); + else if (preset.IsUpscaleTool) + ShowUpscaleWindow(files, preset, near); // Before the crop: the print studio and the PDF page tools have no Tool of their own, which reads as a crop // ("Crop PDF" is one and opens the crop overlay). else if (preset.Id == "tool-sheet-studio") @@ -284,6 +288,38 @@ public void ShowImageEditWindow(IReadOnlyList files, Preset preset) win.Activate(); } + /// + /// Opens the "Upscale Quality" window on the pictures among the files. + /// + public void ShowUpscaleWindow(IReadOnlyList files, Preset preset, PixelPoint? near = null) + { + var pictures = files.Where(f => FormatRegistry.Detect(f) is { } format && UpscaleTool.CanUpscale(format.Id)).ToList(); + if (pictures.Count == 0) + return; + if (_upscaleWindow is { DataContext: IDisposable oldVm }) + { + oldVm.Dispose(); + } + _upscaleWindow?.Close(); + var store = services.GetRequiredService(); + var conversions = services.GetRequiredService(); + var log = services.GetService>(); + var vm = new UpscaleViewModel(pictures, preset, loc, store, PictureSize, + (files, conversion) => conversions.Start(files, conversion), log); + var win = new UpscaleWindow { DataContext = vm }; + PrepareWindow(win); + win.Closed += (_, _) => + { + if (_upscaleWindow == win) + _upscaleWindow = null; + vm.Dispose(); + trimmer.Request(); + }; + _upscaleWindow = win; + win.Show(); + win.Activate(); + } + /// /// Opens the "Pixelate / blur" window on the pictures among the files. Each picture is written by an ordinary /// conversion, shown in the toast. diff --git a/src/Filee.App/ViewModels/Pages/PresetsPageViewModel.cs b/src/Filee.App/ViewModels/Pages/PresetsPageViewModel.cs index e8e92df8..34435aa4 100644 --- a/src/Filee.App/ViewModels/Pages/PresetsPageViewModel.cs +++ b/src/Filee.App/ViewModels/Pages/PresetsPageViewModel.cs @@ -25,7 +25,8 @@ public void Refresh(ILocalizer l) } private static string TargetLabel(Preset p, ILocalizer l) => - p.TargetFormat == BuiltInData.SameAsSource ? l["presets.same_as_source"] + p.IsUpscaleTool ? l["preset.upscale"] + : p.TargetFormat == BuiltInData.SameAsSource ? l["presets.same_as_source"] : "→ " + (FormatLabels.NameOf(l, p.TargetFormat) ?? p.TargetFormat); } @@ -46,7 +47,7 @@ public PresetsPageViewModel(UserDataStore store, ILocalizer loc, WindowService w VisiblePresets = new FilteredList(Presets, p => [p.Name, p.Target, p.Preset.TargetFormat]); var off = store.Settings.TurnedOffFieldSet(); // Groups and tools convert nothing: they live on the donut only (Settings → Toolbar). - foreach (var preset in store.Presets.Where(p => !p.IsGroup && !p.IsTool && FormatFields.IsShown(p.TargetFormat, off))) + foreach (var preset in store.Presets.Where(p => !p.IsGroup && (!p.IsTool || p.IsUpscaleTool) && FormatFields.IsShown(p.TargetFormat, off))) Presets.Add(new PresetListItem(preset, loc)); SelectedPreset = Presets.FirstOrDefault(); PlacePresets = [.. BuiltInData.PlacePresets().Where(p => FormatFields.IsShown(p.TargetFormat, off)) diff --git a/src/Filee.App/ViewModels/PresetEditorViewModel.cs b/src/Filee.App/ViewModels/PresetEditorViewModel.cs index edec844d..9b860a73 100644 --- a/src/Filee.App/ViewModels/PresetEditorViewModel.cs +++ b/src/Filee.App/ViewModels/PresetEditorViewModel.cs @@ -12,6 +12,7 @@ using Filee.Core.Settings; using Filee.Engines.Ocr; using Filee.Engines.Translation; +using Filee.Engines.Upscale; namespace Filee.App.ViewModels; @@ -232,6 +233,9 @@ .. WhisperLanguages.All ]; _targetLanguage = TranslationLanguageChoices.FirstOrDefault(l => string.Equals(l.Value, _edit.Translate.TargetLanguage, StringComparison.OrdinalIgnoreCase)) ?? TranslationLanguageChoices[0]; + _upscaleScale = _edit.Upscale.Scale > 0 ? _edit.Upscale.Scale : 4; + _selectedUpscaleModel = UpscaleModels.FirstOrDefault(m => string.Equals(m.Id, _edit.Upscale.Model, StringComparison.OrdinalIgnoreCase)) + ?? UpscaleModels.FirstOrDefault(); _archiveLevel = ArchiveLevels.First(l => l.Value == _edit.Archive.Level); _combineIntoOne = _edit.Archive.CombineIntoOne; _saveEveryFrame = _edit.Media.SaveEveryFrame; @@ -364,6 +368,12 @@ public async Task TestTranslationAsync() } } + public bool ShowUpscale => _original.IsUpscaleTool || _edit.Upscale.Enabled; + public IReadOnlyList UpscaleModels { get; } = Filee.Engines.Upscale.UpscaleModels.All; + [ObservableProperty] private UpscaleModel? _selectedUpscaleModel; + [ObservableProperty] private int _upscaleScale = 4; + + /// What "Recognize text" needs: Tesseract, or on a Mac Apple Vision (Tesseract only for other languages). public string OcrHint { get; } @@ -1185,6 +1195,12 @@ static double Seconds(double value, double fallback, double min, double max) => _original.Cad.Colors = ShowCadColors ? CadColors.Value : default; _original.Translate.TargetLanguage = TargetLanguage.Value; _original.Translate.Enabled = ShowTranslate; + if (ShowUpscale) + { + _original.Upscale.Enabled = true; + _original.Upscale.Model = SelectedUpscaleModel?.Id; + _original.Upscale.Scale = UpscaleScale; + } var output = _original.Output; // Asking keeps the location it had: a Filee that can't ask (or one from before it) saves there. diff --git a/src/Filee.App/ViewModels/UpscaleViewModel.cs b/src/Filee.App/ViewModels/UpscaleViewModel.cs new file mode 100644 index 00000000..9a938434 --- /dev/null +++ b/src/Filee.App/ViewModels/UpscaleViewModel.cs @@ -0,0 +1,273 @@ +// ViewModel for the "Upscale Quality" tool window. + +using System.Collections.ObjectModel; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Filee.Core.Conversion; +using Filee.Core.Formats; +using Filee.Core.Localization; +using Filee.Core.Presets; +using Filee.Core.Settings; +using Filee.Engines.Upscale; +using Microsoft.Extensions.Logging; + +namespace Filee.App.ViewModels; + +public sealed partial class UpscaleViewModel : ObservableObject, IDisposable +{ + private readonly Preset _tool; + private readonly ILocalizer _loc; + private readonly UserDataStore _store; + private readonly Func _pictureSize; + private readonly Action, Preset> _start; + private readonly ILogger? _log; + private CancellationTokenSource? _cts; + + public event Action? CloseRequested; + + public IReadOnlyList Pictures { get; } + + public string Heading { get; } + public string? HeadingTip { get; } + + public ObservableCollection Models { get; } = []; + public ObservableCollection> Targets { get; } = []; + + private (int Width, int Height)? _initialDimensions; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ResolutionPreview))] + private int _scale = 4; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsSelectedModelInstalled), nameof(ModelStatusText))] + private UpscaleModel? _selectedModel; + + [ObservableProperty] + private Choice _target = null!; + + [ObservableProperty] + private bool _isProcessing; + + [ObservableProperty] + private double _progressValue; + + [ObservableProperty] + private string _progressText = ""; + + [ObservableProperty] + private bool _isCompleted; + + [ObservableProperty] + private string _statusMessage = ""; + + [ObservableProperty] + private bool _hasError; + + public bool IsSelectedModelInstalled => SelectedModel is not null && UpscaleModels.IsInstalled(SelectedModel.Id); + + public string ModelStatusText + { + get + { + if (SelectedModel is null) + return ""; + if (UpscaleModels.IsInstalled(SelectedModel.Id)) + return _loc["upscale.model_ready"]; + var sizeMb = (SelectedModel.TotalSize / 1_000_000.0).ToString("0.0", System.Globalization.CultureInfo.InvariantCulture); + return _loc.Format("upscale.model_download_size", sizeMb); + } + } + + public string ResolutionPreview + { + get + { + if (_initialDimensions is { } dim) + { + var targetW = dim.Width * Scale; + var targetH = dim.Height * Scale; + return _loc.Format("upscale.resolution_preview", dim.Width, dim.Height, targetW, targetH, Scale); + } + return $"{Scale}×"; + } + } + + public UpscaleViewModel( + IReadOnlyList pictures, + Preset tool, + ILocalizer loc, + UserDataStore store, + Func pictureSize, + Action, Preset> start, + ILogger? log = null) + { + Pictures = pictures; + _tool = tool; + _loc = loc; + _store = store; + _pictureSize = pictureSize; + _start = start; + _log = log; + + Heading = pictures.Count == 1 + ? Path.GetFileName(pictures[0]) + : loc.Format("home.files", pictures.Count); + HeadingTip = pictures.Count == 1 ? pictures[0] : string.Join("\n", pictures); + + if (pictures.Count > 0) + { + _initialDimensions = _pictureSize(pictures[0]); + } + + Scale = tool.Upscale.Scale >= 2 ? Math.Clamp(tool.Upscale.Scale, 2, 4) : 4; + + InitTargets(); + RefreshModels(); + } + + private void InitTargets() + { + Targets.Clear(); + var firstFormatId = Pictures.Count > 0 ? (FormatRegistry.Detect(Pictures[0])?.Id ?? "png") : "png"; + Targets.Add(new("*", _loc.Format("upscale.format_same", firstFormatId))); + Targets.Add(new("png", "PNG (.png)")); + Targets.Add(new("jpg", "JPEG (.jpg)")); + Targets.Add(new("webp", "WebP (.webp)")); + Targets.Add(new("avif", "AVIF (.avif)")); + Targets.Add(new("heic", "HEIC (.heic)")); + Targets.Add(new("bmp", "BMP (.bmp)")); + Targets.Add(new("tiff", "TIFF (.tiff)")); + Targets.Add(new("ico", "ICO (.ico)")); + Target = Targets[0]; + } + + public void RefreshModels() + { + Models.Clear(); + foreach (var m in UpscaleModels.All) + { + Models.Add(m); + } + + var initialId = !string.IsNullOrWhiteSpace(_tool.Upscale.Model) ? _tool.Upscale.Model : UpscaleModels.Default; + SelectedModel = Models.FirstOrDefault(m => string.Equals(m.Id, initialId, StringComparison.OrdinalIgnoreCase)) + ?? Models.FirstOrDefault(); + + OnPropertyChanged(nameof(IsSelectedModelInstalled)); + OnPropertyChanged(nameof(ModelStatusText)); + } + + public void NotifyModelStatusChanged() + { + OnPropertyChanged(nameof(IsSelectedModelInstalled)); + OnPropertyChanged(nameof(ModelStatusText)); + } + + [RelayCommand] + public async Task StartUpscaleAsync() + { + if (IsProcessing || SelectedModel is null || Pictures.Count == 0) + return; + + if (UpscalerConverter.LocateExecutable() is null) + { + HasError = true; + StatusMessage = _loc["error.upscale_needs_engine"]; + return; + } + + IsProcessing = true; + IsCompleted = false; + HasError = false; + StatusMessage = ""; + ProgressValue = 0; + + _cts?.Dispose(); + _cts = new CancellationTokenSource(); + var token = _cts.Token; + + var chosenTarget = Target.Value; + var model = SelectedModel; + var currentScale = Math.Clamp(Scale, 2, 4); + + try + { + if (!UpscaleModels.IsInstalled(model.Id)) + { + ProgressText = _loc["upscale.downloading_model"]; + var progress = new Progress(p => + { + Dispatcher.UIThread.Post(() => + { + var pct = (int)Math.Clamp(p.Fraction * 100.0, 0, 100); + ProgressValue = pct; + ProgressText = $"{_loc["upscale.downloading_model"]} ({pct}%)"; + }); + }); + await UpscaleModels.EnsureAsync(model.Id, progress, token); + NotifyModelStatusChanged(); + } + + Remember(model.Id, currentScale); + + foreach (var group in Pictures.GroupBy(path => UpscaleTool.TargetFor(FormatRegistry.Detect(path)?.Id ?? "", chosenTarget))) + { + _start([.. group], UpscaleTool.ConversionOf(_tool, group.Key, model.Id, currentScale)); + } + + CloseRequested?.Invoke(); + } + catch (OperationCanceledException) + { + StatusMessage = ""; + ProgressText = ""; + } + catch (Exception ex) + { + HasError = true; + StatusMessage = _loc.Format("error.upscale_failed", ex.Message); + _log?.LogError(ex, "Upscaling failed"); + } + finally + { + IsProcessing = false; + } + } + + private void Remember(string modelId, int scale) + { + _tool.Upscale.Model = modelId; + _tool.Upscale.Scale = scale; + if (_store.FindPreset(_tool.Id) is { IsUpscaleTool: true } saved) + { + saved.Upscale.Model = modelId; + saved.Upscale.Scale = scale; + try + { + _store.SaveLibrary(); + } + catch (Exception ex) + { + _log?.LogWarning(ex, "Could not save upscale tool settings"); + } + } + } + + [RelayCommand] + public void Cancel() + { + if (IsProcessing && _cts is not null) + { + _cts.Cancel(); + } + } + + public void Dispose() + { + _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; + } +} diff --git a/src/Filee.App/Views/PresetEditorView.axaml b/src/Filee.App/Views/PresetEditorView.axaml index 3ec4d5c4..51f0ec8a 100644 --- a/src/Filee.App/Views/PresetEditorView.axaml +++ b/src/Filee.App/Views/PresetEditorView.axaml @@ -5,6 +5,7 @@ @@ -274,6 +275,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Filee.App/Views/UpscaleWindow.axaml b/src/Filee.App/Views/UpscaleWindow.axaml new file mode 100644 index 00000000..0ffb4752 --- /dev/null +++ b/src/Filee.App/Views/UpscaleWindow.axaml @@ -0,0 +1,113 @@ + + + + M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M7,6H17V19H7V6M9,8V17H11V8H9M13,8V17H15V8H13Z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Filee.App/Views/UpscaleWindow.axaml.cs b/src/Filee.App/Views/UpscaleWindow.axaml.cs new file mode 100644 index 00000000..be860f14 --- /dev/null +++ b/src/Filee.App/Views/UpscaleWindow.axaml.cs @@ -0,0 +1,61 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Filee.App.Services; +using Filee.App.ViewModels; +using Filee.Core.Localization; +using Filee.Engines.Upscale; + +namespace Filee.App.Views; + +public partial class UpscaleWindow : Window +{ + public UpscaleWindow() + { + InitializeComponent(); + Closing += (_, _) => + { + if (DataContext is IDisposable disposable) + { + disposable.Dispose(); + } + }; + DataContextChanged += (_, _) => + { + if (DataContext is UpscaleViewModel vm) + { + vm.CloseRequested += Close; + } + }; + } + + private void OnCancel(object? sender, RoutedEventArgs e) + { + if (DataContext is UpscaleViewModel vm && vm.IsProcessing) + { + vm.Cancel(); + } + Close(); + } + + private async void OnDeleteModel(object? sender, RoutedEventArgs e) + { + if (DataContext is not UpscaleViewModel vm || vm.SelectedModel is null) + return; + + var loc = AppHost.Get(); + var windows = AppHost.Get(); + var message = loc.Format("engines.model.delete_confirm", vm.SelectedModel.Name); + try + { + if (await windows.ConfirmAsync(this, message, destructive: true)) + { + UpscaleModels.Uninstall(vm.SelectedModel.Id); + vm.NotifyModelStatusChanged(); + } + } + catch (Exception ex) + { + await windows.MessageAsync(this, ex.Message); + } + } +} diff --git a/src/Filee.Core/Conversion/ConversionJob.cs b/src/Filee.Core/Conversion/ConversionJob.cs index ac1dda7d..bce3df60 100644 --- a/src/Filee.Core/Conversion/ConversionJob.cs +++ b/src/Filee.Core/Conversion/ConversionJob.cs @@ -87,7 +87,7 @@ public sealed class FileResult /// The source, target and constraints of a route planning request. public sealed record RouteRequest(string Source, string Target, bool Frames = false, bool Shaping = false, - bool MotionPhoto = false, bool LivePhoto = false, bool Ocr = false, bool Translate = false); + bool MotionPhoto = false, bool LivePhoto = false, bool Ocr = false, bool Translate = false, bool Upscale = false); /// A batch of files converted with one preset. public sealed class ConversionJob diff --git a/src/Filee.Core/Conversion/IConverter.cs b/src/Filee.Core/Conversion/IConverter.cs index 26a88a31..d777602a 100644 --- a/src/Filee.Core/Conversion/IConverter.cs +++ b/src/Filee.Core/Conversion/IConverter.cs @@ -61,7 +61,7 @@ namespace Filee.Core.Conversion; public readonly record struct ConversionEdge( string From, string To, int Cost = 10, bool ImageSourcesOnly = false, bool NotForImageSources = false, bool DirectOnly = false, bool FrameSequence = false, bool Unshaped = false, bool MotionPhoto = false, bool LivePhoto = false, - bool Ocr = false, bool Caption = false, bool Translate = false); + bool Ocr = false, bool Caption = false, bool Translate = false, bool Upscale = false); /// /// A conversion an engine refuses with a reason the user can act on, shown in their language (the file's @@ -146,7 +146,8 @@ public sealed record ConversionStep( Action>? MissingFonts = null, string? OriginalSourcePath = null, bool Caption = false, - bool Translate = false); + bool Translate = false, + bool Upscale = false); /// /// A conversion engine (ImageMagick, LibreOffice, rhwp, ...). diff --git a/src/Filee.Core/Conversion/JobQueue.cs b/src/Filee.Core/Conversion/JobQueue.cs index 85e5849b..85dbb501 100644 --- a/src/Filee.Core/Conversion/JobQueue.cs +++ b/src/Filee.Core/Conversion/JobQueue.cs @@ -803,15 +803,18 @@ private async Task> RunFileAsync( var translate = finalOutput && runWith.Translate.Enabled; if (translate && !runWith.TranslatesDocument(from.Id)) return Fail(file, "error.no_route", $"{from.Id} → {target}", new RouteRequest(from.Id, target, Translate: true)); + var upscale = finalOutput && runWith.Upscale.Enabled; + if (upscale && !UpscaleTool.CanUpscale(from.Id)) + return Fail(file, "error.no_route", $"{from.Id} → {target}", new RouteRequest(from.Id, target, Upscale: true)); // "Recognize text": Tesseract writes a searchable PDF first, read on into DOCX, TXT, … from there. Also for the // PDFs that are merged into one. var ocr = (finalOutput || target == "pdf") && runWith.RecognizesTextOf(from.Id); var route = opener is null - ? planner.Plan(source.Id, target, frames, motionPhoto: motionPhoto, livePhoto: livePhoto, ocr: ocr, caption: caption, translate: translate) - : planner.PlanThrough(source.Id, from.Id, target, frames, motionPhoto: motionPhoto, livePhoto: livePhoto, ocr: ocr, caption: caption, translate: translate); + ? planner.Plan(source.Id, target, frames, motionPhoto: motionPhoto, livePhoto: livePhoto, ocr: ocr, caption: caption, translate: translate, upscale: upscale) + : planner.PlanThrough(source.Id, from.Id, target, frames, motionPhoto: motionPhoto, livePhoto: livePhoto, ocr: ocr, caption: caption, translate: translate, upscale: upscale); if (route is null) return Fail(file, "error.no_route", $"{from.Id} → {target}", - new RouteRequest(from.Id, target, Frames: frames, MotionPhoto: motionPhoto, LivePhoto: livePhoto, Ocr: ocr, Translate: translate)); + new RouteRequest(from.Id, target, Frames: frames, MotionPhoto: motionPhoto, LivePhoto: livePhoto, Ocr: ocr, Translate: translate, Upscale: upscale)); // A step that draws text without shaping (rhwp) would break Arabic, Hebrew, Indic or Thai text: such a file // takes a route without it (LibreOffice), or is refused with the reason rather than written broken. One @@ -907,7 +910,7 @@ private async Task> RunFileAsync( file.MissingFonts = file.MissingFonts.Concat(fonts) .Distinct(StringComparer.OrdinalIgnoreCase).ToList(); Notify(job); - }, file.SourcePath, Caption: step.Caption, Translate: step.Translate); + }, file.SourcePath, Caption: step.Caption, Translate: step.Translate, Upscale: step.Upscale); outputs.AddRange(await step.Converter.ConvertAsync(stepInfo, progress, ct)); } finally diff --git a/src/Filee.Core/Conversion/RoutePlanner.cs b/src/Filee.Core/Conversion/RoutePlanner.cs index 4cfe9ba3..123735cb 100644 --- a/src/Filee.Core/Conversion/RoutePlanner.cs +++ b/src/Filee.Core/Conversion/RoutePlanner.cs @@ -15,7 +15,7 @@ namespace Filee.Core.Conversion; /// The hop makes a Live / Motion Photo of a video (). /// The hop transcribes speech to SRT subtitles using Whisper (). public sealed record RouteStep(IConverter Converter, string From, string To, bool FrameSequence = false, bool Unshaped = false, - bool MotionPhoto = false, bool LivePhoto = false, bool Caption = false, bool Translate = false); + bool MotionPhoto = false, bool LivePhoto = false, bool Caption = false, bool Translate = false, bool Upscale = false); /// A planned conversion from one format to another. public sealed record Route(IReadOnlyList Steps, int Cost) @@ -82,7 +82,7 @@ public RoutePlanner(IEnumerable converters, Func? p /// /// Transcribe speech to SRT subtitles using Whisper (). public Route? Plan(string from, string to, bool frames = false, bool shaping = false, bool motionPhoto = false, bool livePhoto = false, - bool ocr = false, bool caption = false, bool translate = false) + bool ocr = false, bool caption = false, bool translate = false, bool upscale = false) { from = from.ToLowerInvariant(); to = to.ToLowerInvariant(); @@ -93,6 +93,28 @@ public RoutePlanner(IEnumerable converters, Func? p if (translate) return PlanWithTranslate(from, to, shaping); + if (upscale) + { + var direct = EdgesFrom(from) + .Where(e => e.Edge.To == to && e.Edge.Upscale) + .OrderBy(e => e.Cost) + .FirstOrDefault(); + if (direct.Converter is not null) + return new Route([new RouteStep(direct.Converter, from, to, Upscale: true)], direct.Cost); + + var toPng = Plan(from, "png", shaping: shaping); + if (toPng is not null) + { + var fromPng = EdgesFrom("png") + .Where(e => e.Edge.To == to && e.Edge.Upscale) + .OrderBy(e => e.Cost) + .FirstOrDefault(); + if (fromPng.Converter is not null) + return new Route([.. toPng.Steps, new RouteStep(fromPng.Converter, "png", to, Upscale: true)], toPng.Cost + fromPng.Cost); + } + return null; + } + if (frames || motionPhoto || livePhoto || caption) { // One of them: frames, then the video of a photo, then a photo of a video, then caption. @@ -113,7 +135,7 @@ public RoutePlanner(IEnumerable converters, Func? p if (from == to) { var best = EdgesFrom(from) - .Where(e => e.Edge.To == to && !e.Edge.FrameSequence && !e.Edge.MotionPhoto && !e.Edge.LivePhoto && !e.Edge.Ocr && !e.Edge.Caption && !e.Edge.Translate) + .Where(e => e.Edge.To == to && !e.Edge.FrameSequence && !e.Edge.MotionPhoto && !e.Edge.LivePhoto && !e.Edge.Ocr && !e.Edge.Caption && !e.Edge.Translate && !e.Edge.Upscale) .OrderBy(e => e.Cost) .FirstOrDefault(); return best.Converter is null @@ -142,7 +164,7 @@ public RoutePlanner(IEnumerable converters, Func? p foreach (var (converter, edge, edgeCost) in EdgesFrom(node)) { - if (edge.To == edge.From || edge.FrameSequence || edge.MotionPhoto || edge.LivePhoto || edge.Ocr || edge.Translate || edge.Caption + if (edge.To == edge.From || edge.FrameSequence || edge.MotionPhoto || edge.LivePhoto || edge.Ocr || edge.Translate || edge.Caption || edge.Upscale || (edge.ImageSourcesOnly && !imageSource) || (edge.NotForImageSources && imageSource) || (edge.Unshaped && shaping) || (edge.DirectOnly && (node != from || edge.To != to))) @@ -174,8 +196,8 @@ public RoutePlanner(IEnumerable converters, Func? p /// A Live / Motion Photo of a video (see ). /// Recognize the text first (see ). /// Transcribe speech to SRT subtitles using Whisper (see ). - public bool CanConvert(string from, string to, bool frames = false, bool motionPhoto = false, bool livePhoto = false, bool ocr = false, bool caption = false, bool translate = false) => - Plan(from, to, frames, motionPhoto: motionPhoto, livePhoto: livePhoto, ocr: ocr, caption: caption, translate: translate) is not null; + public bool CanConvert(string from, string to, bool frames = false, bool motionPhoto = false, bool livePhoto = false, bool ocr = false, bool caption = false, bool translate = false, bool upscale = false) => + Plan(from, to, frames, motionPhoto: motionPhoto, livePhoto: livePhoto, ocr: ocr, caption: caption, translate: translate, upscale: upscale) is not null; /// /// The route for an envelope (): the cheapest edge from to the @@ -184,7 +206,7 @@ public bool CanConvert(string from, string to, bool frames = false, bool motionP /// file inside can't become . /// public Route? PlanThrough(string from, string through, string to, bool frames = false, bool shaping = false, bool motionPhoto = false, - bool livePhoto = false, bool ocr = false, bool caption = false, bool translate = false) + bool livePhoto = false, bool ocr = false, bool caption = false, bool translate = false, bool upscale = false) { from = from.ToLowerInvariant(); through = through.ToLowerInvariant(); @@ -196,9 +218,9 @@ public bool CanConvert(string from, string to, bool frames = false, bool motionP if (open.Converter is null) return null; var step = new RouteStep(open.Converter, from, through); - if (through == to && !(frames || motionPhoto || livePhoto || ocr || caption || translate)) + if (through == to && !(frames || motionPhoto || livePhoto || ocr || caption || translate || upscale)) return new Route([step], open.Cost); - return Plan(through, to, frames, shaping, motionPhoto, livePhoto, ocr, caption, translate) is { } rest + return Plan(through, to, frames, shaping, motionPhoto, livePhoto, ocr, caption, translate, upscale) is { } rest ? new Route([step, .. rest.Steps], open.Cost + rest.Cost) : null; } diff --git a/src/Filee.Core/Presets/BuiltInData.cs b/src/Filee.Core/Presets/BuiltInData.cs index 6c517d20..c1d3dae6 100644 --- a/src/Filee.Core/Presets/BuiltInData.cs +++ b/src/Filee.Core/Presets/BuiltInData.cs @@ -61,6 +61,7 @@ .. ImageEditPresets(), .. HidePresets(), .. AnnotatePresets(), .. BackgroundPresets(), + .. UpscalePresets(), new() { Id = "tool-sheet-studio", NameKey = "preset.sheet_studio", TargetFormat = "pdf", @@ -330,6 +331,22 @@ public static List BackgroundPresets() => }, ]; + /// Id of the built-in "Upscale" tool (settings schema 69): in the palette, on no donut. + public const string UpscaleId = "tool-upscale"; + + /// + /// "Upscale Quality" (settings schema 69): upscale images using local AI models (Real-ESRGAN / NCNN). + /// In the palette only: the picture donuts are full. + /// + public static List UpscalePresets() => + [ + new() + { + Id = UpscaleId, NameKey = "preset.upscale", TargetFormat = SameAsSource, + Kind = PresetKind.Tool, Tool = Preset.UpscaleTool, Output = { FileNamePattern = "{name}_upscaled" }, + }, + ]; + /// /// "Trim transparent borders" (issue #332, added in settings schema 33): a copy cropped to the visible pixels, in the /// same format. On the images donut's back face after "Remove background". diff --git a/src/Filee.Core/Presets/Preset.cs b/src/Filee.Core/Presets/Preset.cs index be830f7c..1c97e6e6 100644 --- a/src/Filee.Core/Presets/Preset.cs +++ b/src/Filee.Core/Presets/Preset.cs @@ -129,6 +129,9 @@ public sealed class Preset /// Options used for offline AI translation. public TranslateOptions Translate { get; set; } = new(); + /// Options used for local AI image upscaling. + public UpscaleOptions Upscale { get; set; } = new(); + /// Where and how output files are written. public OutputRule Output { get; set; } = new(); @@ -278,10 +281,15 @@ public bool RecognizesTextOf(string sourceFormat) => /// public const string BackgroundTool = "background"; + /// + /// of "Upscale" (settings schema 69): enhance image quality and resolution using local AI models. + /// + public const string UpscaleTool = "upscale"; + /// /// : which window it opens, (or null, as saved before there /// were other tools), , , , - /// or . A tool this version doesn't know (from a newer + /// , or . A tool this version doesn't know (from a newer /// Filee) opens nothing. Ignored for other kinds. /// public string? Tool { get; set; } @@ -309,6 +317,10 @@ public bool RecognizesTextOf(string sourceFormat) => [JsonIgnore] public bool IsBackgroundTool => IsTool && Tool == BackgroundTool; + /// True for "Upscale". + [JsonIgnore] + public bool IsUpscaleTool => IsTool && Tool == UpscaleTool; + /// /// True if converting a file cuts a clip of it (, /// set by the clip window on the conversion it starts): the file is a video. Other files convert as usual. @@ -366,6 +378,7 @@ public Preset ForSharing() Trace = Trace.Clone(), Cad = Cad.Clone(), Translate = Translate.Clone(), + Upscale = Upscale.Clone(), Output = Output.Clone(), Kind = Kind, Tool = Tool, @@ -1677,3 +1690,13 @@ public sealed class TranslateOptions public TranslateOptions Clone() => (TranslateOptions)MemberwiseClone(); } + +/// Options for local AI image upscaling (Real-ESRGAN / NCNN). +public sealed class UpscaleOptions +{ + public bool Enabled { get; set; } + public string? Model { get; set; } + public int Scale { get; set; } = 4; + + public UpscaleOptions Clone() => (UpscaleOptions)MemberwiseClone(); +} diff --git a/src/Filee.Core/Presets/UpscaleTool.cs b/src/Filee.Core/Presets/UpscaleTool.cs new file mode 100644 index 00000000..9f2e9415 --- /dev/null +++ b/src/Filee.Core/Presets/UpscaleTool.cs @@ -0,0 +1,49 @@ +// "Upscale Quality": upscale images using local AI models (Real-ESRGAN / NCNN). + +using Filee.Core.Conversion; +using Filee.Core.Formats; + +namespace Filee.Core.Presets; + +/// Turns the "Upscale Quality" tool into the conversions that write the upscaled pictures. +public static class UpscaleTool +{ + /// Where an image goes when its own format cannot be written directly. + public const string FallbackTarget = "png"; + + /// The formats the upscale window offers, after "Same format as the source". + public static readonly IReadOnlyList Targets = ["png", "jpg", "webp", "avif", "heic", "ico", "bmp", "tiff"]; + + /// True when a file is an image the tool can upscale. + public static bool CanUpscale(string sourceFormat) => + (FormatRegistry.FindById(sourceFormat) ?? FormatRegistry.FindByExtension(sourceFormat))?.Category == FormatCategory.Image; + + /// + /// Resolves the output format: either the chosen target or preserves the source format when possible. + /// + public static string TargetFor(string sourceFormat, string chosen) + { + if (chosen != BuiltInData.SameAsSource) + return chosen; + var detected = FormatRegistry.FindById(sourceFormat) ?? FormatRegistry.FindByExtension(sourceFormat); + return detected is not null && Targets.Contains(detected.Id) ? detected.Id : FallbackTarget; + } + + /// + /// Creates the conversion preset that writes upscaled pictures as . + /// + public static Preset ConversionOf(Preset tool, string targetFormat, string? model, int scale) + { + var preset = tool.Clone(); + preset.Kind = PresetKind.Convert; + preset.Tool = null; + preset.TargetFormat = targetFormat; + preset.Upscale = new UpscaleOptions + { + Enabled = true, + Model = model, + Scale = scale, + }; + return preset; + } +} diff --git a/src/Filee.Core/Settings/AppSettings.cs b/src/Filee.Core/Settings/AppSettings.cs index 147094f0..2e7ccb4e 100644 --- a/src/Filee.Core/Settings/AppSettings.cs +++ b/src/Filee.Core/Settings/AppSettings.cs @@ -123,7 +123,7 @@ public static bool IsFlipKeyAllowed(string key, IEnumerable gest public sealed class AppSettings { /// Current schema version. Increase when the format changes and add a migration. - public const int CurrentSchemaVersion = 68; + public const int CurrentSchemaVersion = 69; public int SchemaVersion { get; set; } = CurrentSchemaVersion; diff --git a/src/Filee.Core/Settings/UserDataStore.cs b/src/Filee.Core/Settings/UserDataStore.cs index 0d45ed52..8955c8ec 100644 --- a/src/Filee.Core/Settings/UserDataStore.cs +++ b/src/Filee.Core/Settings/UserDataStore.cs @@ -1001,7 +1001,7 @@ public static bool ApplyToLibrary(int from, List presets, List p.Id != BuiltInData.BackgroundId)) { var annotate = presets.FindIndex(p => p.Id == BuiltInData.AnnotateId); @@ -1024,6 +1024,16 @@ public static bool ApplyToLibrary(int from, List presets, List p.Id != BuiltInData.UpscaleId)) + { + var after = presets.FindIndex(p => p.Id == BuiltInData.BackgroundId); + if (after < 0) + after = presets.FindIndex(p => p.Id == BuiltInData.AnnotateId); + presets.InsertRange(after < 0 ? presets.Count : after + 1, BuiltInData.UpscalePresets()); + changed = true; + } return changed; } diff --git a/src/Filee.Engines/EngineRegistry.cs b/src/Filee.Engines/EngineRegistry.cs index b2023612..6df107cb 100644 --- a/src/Filee.Engines/EngineRegistry.cs +++ b/src/Filee.Engines/EngineRegistry.cs @@ -65,6 +65,7 @@ public static IReadOnlyList CreateAll(EngineEnvironment env, ILogger new SubtitleConverter(), new WicImageConverter(), new WhisperConverter(loggers?.CreateLogger()), + new Upscale.UpscalerConverter(loggers?.CreateLogger()), new FfmpegConverter(loggers?.CreateLogger()), new CadConverter(), new ModelConverter(), diff --git a/src/Filee.Engines/Filee.Engines.csproj b/src/Filee.Engines/Filee.Engines.csproj index 0c52aa32..51d3dd60 100644 --- a/src/Filee.Engines/Filee.Engines.csproj +++ b/src/Filee.Engines/Filee.Engines.csproj @@ -58,5 +58,6 @@ + diff --git a/src/Filee.Engines/Infrastructure/EngineDownloads.cs b/src/Filee.Engines/Infrastructure/EngineDownloads.cs index d4a90413..e1bb1948 100644 --- a/src/Filee.Engines/Infrastructure/EngineDownloads.cs +++ b/src/Filee.Engines/Infrastructure/EngineDownloads.cs @@ -54,6 +54,7 @@ internal static string DownloadRuntime(string runtimeIdentifier, int windowsBuil new("ffmpeg", ["ffmpeg"], 272_000_000, ["ffmpeg"]), new("calibre", ["calibre"], 663_000_000, ["calibre"]), new("tesseract", ["tesseract"], 168_000_000, ["tesseract"]), + new("upscayl-bin", OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture != Architecture.Arm64 ? ["upscayl-bin", "vcruntime", "vcomp14"] : OperatingSystem.IsWindows() ? [] : ["upscayl-bin"], 8_600_000, ["upscaler"]), .. (OperatingSystem.IsWindows() ? [new EnginePackage("whisper", ["whisper", "whisper-model"], 156_880_105, ["whisper"])] : Array.Empty()), ]; @@ -66,6 +67,8 @@ internal static string DownloadRuntime(string runtimeIdentifier, int windowsBuil { if (CanDownload(package)) return null; + if (package.Id == "upscayl-bin") + return $"Upscale is not available on this PC ({RuntimeIdentifier})."; if (OperatingSystem.IsMacOS()) return package.Id switch { @@ -96,7 +99,7 @@ internal static string DownloadRuntime(string runtimeIdentifier, int windowsBuil /// only needed for EPS / PostScript and for scans. /// public static bool IsSuggested(EnginePackage package) => - CanDownload(package) && DownloadSize(package) < SuggestLimit && package.Id is not ("ghostscript" or "tesseract"); + CanDownload(package) && DownloadSize(package) < SuggestLimit && package.Id is not ("ghostscript" or "tesseract" or "upscayl-bin"); /// The running macOS; downloads whose minimumMacOS is newer are left out. Null keeps all. public static IReadOnlyDictionary ComponentsForRuntime(string runtimeIdentifier, Version? macOSVersion = null) diff --git a/src/Filee.Engines/Infrastructure/EngineEnvironment.cs b/src/Filee.Engines/Infrastructure/EngineEnvironment.cs index baf5eaa2..90f01de3 100644 --- a/src/Filee.Engines/Infrastructure/EngineEnvironment.cs +++ b/src/Filee.Engines/Infrastructure/EngineEnvironment.cs @@ -89,6 +89,7 @@ dataDirectoryOverride is not null ["7zip"] = [windows ? "7z.exe" : "7zz"], ["tesseract"] = [windows ? "tesseract.exe" : "tesseract"], ["whisper"] = [windows ? "whisper-cli.exe" : "whisper-cli"], + ["upscayl-bin"] = [windows ? "upscayl-bin.exe" : "upscayl-bin"], }; public static string ProgramName(string name) => OperatingSystem.IsWindows() ? name diff --git a/src/Filee.Engines/Infrastructure/EngineInstaller.cs b/src/Filee.Engines/Infrastructure/EngineInstaller.cs index a2eade7d..683c2d4f 100644 --- a/src/Filee.Engines/Infrastructure/EngineInstaller.cs +++ b/src/Filee.Engines/Infrastructure/EngineInstaller.cs @@ -78,15 +78,18 @@ public sealed class EngineInstaller private readonly HttpClient _http; private readonly IReadOnlyDictionary _components; private readonly ILogger _logger; + private readonly IReadOnlyList _packages; /// Folder that receives one sub folder per engine (default: EngineEnvironment.DownloadRoot). /// Download list; engines.json when null (tests pass their own). - public EngineInstaller(HttpClient http, string? root = null, IReadOnlyDictionary? components = null, ILogger? logger = null) + /// Known packages (tests pass their own); EngineDownloads.Packages when null. + public EngineInstaller(HttpClient http, string? root = null, IReadOnlyDictionary? components = null, ILogger? logger = null, IReadOnlyList? packages = null) { _http = http; _root = root ?? EngineEnvironment.DownloadRoot; _components = components ?? EngineDownloads.Components; _logger = logger ?? NullLogger.Instance; + _packages = packages ?? EngineDownloads.Packages; } /// @@ -137,7 +140,7 @@ public async Task InstallAsync(EnginePackage package, IProgress !string.Equals(Marker(c), _components[c].Sha256, StringComparison.OrdinalIgnoreCase)) .Select(c => _components[c]) @@ -179,16 +182,23 @@ public async Task InstallAsync(EnginePackage package, IProgress p.Id != package.Id && IsInstalled(p)) + .SelectMany(p => p.Components) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var componentsToRemove = package.Components.Where(c => !shared.Contains(c)).ToList(); + + ClearAside(componentsToRemove); var aside = new List<(string Folder, string Moved)>(); try { - foreach (var id in package.Components) + foreach (var folder in componentsToRemove.Select(FolderOf).Distinct()) { - var folder = FolderOf(id); + var folderName = Path.GetFileName(folder); if (!Directory.Exists(folder)) continue; - var moved = AsideFolder(id); + var moved = AsideFolder(folderName); Directory.Move(folder, moved); aside.Add((folder, moved)); } @@ -210,13 +220,16 @@ public void Uninstall(EnginePackage package) private string AsideFolder(string id) => Path.Combine(_root, $"{AsidePrefix}{id}-{Guid.NewGuid().ToString("N")[..8]}"); /// Deletes what an earlier removal or update of the package left aside (under the package's lock). - private void ClearAside(EnginePackage package) + private void ClearAside(EnginePackage package) => ClearAside(package.Components); + + /// Deletes what an earlier removal of components left aside (under the package's lock). + private void ClearAside(IEnumerable components) { if (!Directory.Exists(_root)) return; - foreach (var id in package.Components) - foreach (var folder in Directory.EnumerateDirectories(_root, $"{AsidePrefix}{id}-*")) - TryDeleteFolder(folder); + foreach (var folder in components.Select(FolderOf).Distinct()) + foreach (var asideFolder in Directory.EnumerateDirectories(_root, $"{AsidePrefix}{Path.GetFileName(folder)}-*")) + TryDeleteFolder(asideFolder); } /// Deletes a folder that is only in the way: a failure is logged and never replaces another error (#1092). @@ -415,6 +428,14 @@ private Task UnpackAsync(EngineComponent component, string file, CancellationTok File.Copy(file, Path.Combine(staging, component.Id + ".traineddata")); Replace(component.Id, staging); break; + case "ncnn-param": + case "ncnn-bin": + var ncnnTarget = FolderOf(component.Id); + Directory.CreateDirectory(ncnnTarget); + var ncnnFile = Path.GetFileName(new Uri(component.Url).LocalPath); + File.Copy(file, Path.Combine(ncnnTarget, ncnnFile), overwrite: true); + File.WriteAllText(Path.Combine(ncnnTarget, "." + component.Id), component.Sha256); + return; case "model": case "bin": // Standalone AI model file (e.g. Whisper ggml-base.bin): copied directly into its component folder. @@ -669,20 +690,30 @@ internal static void UnpackConda(string condaFile, string target, CancellationTo } /// - /// App-local Microsoft C++ runtime: when a package includes the "vcruntime" component, its DLLs are copied next - /// to the programs of the package's other conda components. Windows looks for DLLs in a program's folder before - /// System32, so the runtime a program was built for is used even when the PC has none or an older one. + /// App-local Microsoft C++ runtime: when a package includes runtime components ("vcruntime", "vcomp14"), their DLLs + /// are copied next to the programs of the package's other conda or zip components. Windows looks for DLLs in a + /// program's folder before System32, so the runtime a program was built for is used even when the PC has none or an older one. /// + private static readonly string[] RuntimeComponents = ["vcruntime", "vcomp14"]; + private void CopyRuntime(EnginePackage package) { - const string runtime = "vcruntime"; - if (!package.Components.Contains(runtime) || !Directory.Exists(FolderOf(runtime))) + var runtimeDirs = RuntimeComponents + .Where(r => package.Components.Contains(r)) + .Select(FolderOf) + .Where(Directory.Exists) + .ToList(); + if (runtimeDirs.Count == 0) return; - var dlls = Directory.GetFiles(FolderOf(runtime), "*.dll"); - foreach (var id in package.Components.Where(c => c != runtime && _components[c].Kind == "conda")) + + var dlls = runtimeDirs.SelectMany(dir => Directory.GetFiles(dir, "*.dll")).ToList(); + foreach (var id in package.Components.Where(c => !RuntimeComponents.Contains(c) && (_components[c].Kind is "conda" or "zip"))) { + var targetDir = FolderOf(id); + if (!Directory.Exists(targetDir)) + continue; foreach (var dll in dlls) - File.Copy(dll, Path.Combine(FolderOf(id), Path.GetFileName(dll)), overwrite: true); + File.Copy(dll, Path.Combine(targetDir, Path.GetFileName(dll)), overwrite: true); } } @@ -724,7 +755,14 @@ private void Replace(string id, string source) /// Folder of a component; the LibreOffice extension lives inside LibreOffice. private string FolderOf(string id) => _components.TryGetValue(id, out var component) && component.Kind == "oxt" ? Path.Combine(_root, "libreoffice", "share", "extensions", ExtensionFolder(id)) - : Path.Combine(_root, id); + : _components.TryGetValue(id, out var c) && c.Kind is "ncnn-param" or "ncnn-bin" + ? Path.Combine(_root, NcnnModelFolder(id)) + : Path.Combine(_root, id); + + private static string NcnnModelFolder(string id) => + id.EndsWith("-param", StringComparison.Ordinal) ? id[..^6] + : id.EndsWith("-bin", StringComparison.Ordinal) ? id[..^4] + : id; private static string ExtensionFolder(string id) => id switch { @@ -735,6 +773,11 @@ private string FolderOf(string id) => _components.TryGetValue(id, out var compon /// Hash of the installed version, or null when not installed. private string? Marker(string id) { + if (_components.TryGetValue(id, out var component) && component.Kind is "ncnn-param" or "ncnn-bin") + { + var componentMarker = Path.Combine(FolderOf(id), "." + id); + return File.Exists(componentMarker) ? File.ReadAllText(componentMarker).Trim() : null; + } var marker = Path.Combine(FolderOf(id), MarkerName); return File.Exists(marker) ? File.ReadAllText(marker).Trim() : null; } diff --git a/src/Filee.Engines/Infrastructure/engines.json b/src/Filee.Engines/Infrastructure/engines.json index cb1f3b59..bcd98356 100644 --- a/src/Filee.Engines/Infrastructure/engines.json +++ b/src/Filee.Engines/Infrastructure/engines.json @@ -57,6 +57,13 @@ "size": 682908, "kind": "conda" }, + "vcomp14": { + "version": "14.44.35208", + "url": "https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_41.conda", + "sha256": "FA38E30D6EC5D48D559ACDECE81B0CDEE42B8376AFF270C542A5898BA7C46F49", + "size": 114028, + "kind": "conda" + }, "ffmpeg": { "version": "9.0.2", "url": "https://github.com/GyanD/codexffmpeg/releases/download/9.0.2/ffmpeg-9.0.2-full_build-shared.zip", @@ -91,6 +98,13 @@ "sha256": "60ED5BC3DD14EEA856493D334349B405782DDCAF0028D4B5DF4088345FBA2EFE", "size": 147951465, "kind": "model" + }, + "upscayl-bin": { + "version": "20251207", + "url": "https://github.com/upscayl/upscayl-ncnn/releases/download/20251207-174704/upscayl-bin-20251207-174704-windows.zip", + "sha256": "1F0F65C5D2ADE866555E2AC467D35952C35A47080A4060FE56A1AB028F67258A", + "size": 2421760, + "kind": "zip" } }, "platforms": { @@ -130,6 +144,13 @@ "sha256": "953E4512A6482848CD803C88B6D942E6AFCA65DC3B67C7D1353AE048D616DE53", "size": 201590724, "kind": "tar.xz" + }, + "upscayl-bin": { + "version": "20251207", + "url": "https://github.com/upscayl/upscayl-ncnn/releases/download/20251207-174704/upscayl-bin-20251207-174704-linux.zip", + "sha256": "A9FAB3C770B62F2B7A35D8D6D61EB4E8B3AEF79128B665C919D080B85A2292F2", + "size": 3952825, + "kind": "zip" } } }, @@ -209,6 +230,13 @@ "sha256": "135B8A95B8133396D54BF8E726DBC0066145EFA0D785963FC3D4592ACBFCFE5B", "size": 308308875, "kind": "dmg" + }, + "upscayl-bin": { + "version": "20251207", + "url": "https://github.com/upscayl/upscayl-ncnn/releases/download/20251207-174704/upscayl-bin-20251207-174704-macos.zip", + "sha256": "277419791281A56EAE0C739C70120B974D7267CF7C2DE8E86DC09798D4B314DB", + "size": 9338240, + "kind": "zip" } } }, @@ -249,6 +277,13 @@ "sha256": "94BB3248DF074C225490A8A6D1D9DC87C7D6783DBB7A8E9F0D0C3D94348552AF", "size": 297798926, "kind": "dmg" + }, + "upscayl-bin": { + "version": "20251207", + "url": "https://github.com/upscayl/upscayl-ncnn/releases/download/20251207-174704/upscayl-bin-20251207-174704-macos.zip", + "sha256": "277419791281A56EAE0C739C70120B974D7267CF7C2DE8E86DC09798D4B314DB", + "size": 9338240, + "kind": "zip" } } } diff --git a/src/Filee.Engines/Infrastructure/upscale-models.json b/src/Filee.Engines/Infrastructure/upscale-models.json new file mode 100644 index 00000000..878921d2 --- /dev/null +++ b/src/Filee.Engines/Infrastructure/upscale-models.json @@ -0,0 +1,142 @@ +{ + "_comment": "Curated image super-resolution models from upscayl/custom-models, downloaded on demand: pinned URL + SHA-256 per file. Each model consists of a .param and a .bin file.", + "commit": "4b6d2cfa59c7442af115dfc6e50fd8d7d40b96ef", + "url": "https://raw.githubusercontent.com/upscayl/custom-models/4b6d2cfa59c7442af115dfc6e50fd8d7d40b96ef/models/{0}", + "models": { + "4x_NMKD-Superscale-SP_178000_G": { + "name": "NMKD Superscale", + "scale": 4, + "license": "WTFPL", + "author": "Philip Hofmann", + "description": "Clean, artifact-free real-world images.", + "param": { + "sha256": "519BFFB4912E295155D3CB5D6A29639D217CCCA55F80962D8F677EF777A65B7A", + "size": 108037 + }, + "bin": { + "sha256": "6EA113A9C9066B296BA147FA0939F704462BAC000B42E6A79E8FB3B7526D67A4", + "size": 66793352 + } + }, + "4x_NMKD-Siax_200k": { + "name": "NMKD Siax", + "scale": 4, + "license": "WTFPL", + "author": "Philip Hofmann", + "description": "Universal upscaler for photos and slightly compressed images.", + "param": { + "sha256": "519BFFB4912E295155D3CB5D6A29639D217CCCA55F80962D8F677EF777A65B7A", + "size": 108037 + }, + "bin": { + "sha256": "B2ABDFFA30FB15BE752AC681E0C0EDF8B543745C33A989AADC9D7D3394DD3110", + "size": 66793352 + } + }, + "RealESRGAN_General_x4_v3": { + "name": "RealESRGAN General x4 v3", + "scale": 4, + "license": "BSD-3-Clause", + "author": "Xintao Wang", + "description": "Compact and fast general-purpose photo restoration model.", + "param": { + "sha256": "22174924330297357434AD21ED0AF7F4B820008D2A502B492754D130D4142714", + "size": 5019 + }, + "bin": { + "sha256": "85EE266B632A765A725425BA6A5620C088C8AA2939A03063B2D83B3462724CC1", + "size": 2435272 + } + }, + "RealESRGAN_General_WDN_x4_v3": { + "name": "RealESRGAN General WDN x4 v3", + "scale": 4, + "license": "BSD-3-Clause", + "author": "Xintao Wang", + "description": "Wide and deep network model with balanced denoising.", + "param": { + "sha256": "22174924330297357434AD21ED0AF7F4B820008D2A502B492754D130D4142714", + "size": 5019 + }, + "bin": { + "sha256": "74EAE91A6230CBE5B931BE6C49141FAAFF0034976933D4DA9ACCB77C329C8A78", + "size": 2435272 + } + }, + "4xLSDIR": { + "name": "4x LSDIR", + "scale": 4, + "license": "CC-BY-4.0", + "author": "Philip Hofmann (Phhofm)", + "description": "High-fidelity real-world image restoration.", + "param": { + "sha256": "4576ED5C2FC5FA250D3C3D585EF02248F26ABDFC1867088078F501FE71E5D61E", + "size": 108039 + }, + "bin": { + "sha256": "0622F182F0A940B395E4FC70E2707B285E016FA4B014E855205EB40EFDDFB853", + "size": 33424520 + } + }, + "4xLSDIRplusC": { + "name": "4x LSDIR+ C", + "scale": 4, + "license": "CC-BY-4.0", + "author": "Philip Hofmann (Phhofm)", + "description": "Trained on large-scale diverse image restoration with extra detail.", + "param": { + "sha256": "4576ED5C2FC5FA250D3C3D585EF02248F26ABDFC1867088078F501FE71E5D61E", + "size": 108039 + }, + "bin": { + "sha256": "6DF18C714BB7D4AC63CA8A1FB1F1B2712FE3EF5A4B36F09BFA9B827DA167AE81", + "size": 33424520 + } + }, + "4xLSDIRCompactC3": { + "name": "4x LSDIR Compact C3", + "scale": 4, + "license": "CC-BY-4.0", + "author": "Philip Hofmann (Phhofm)", + "description": "SRVGGNet compact model for significantly faster inference.", + "param": { + "sha256": "6E3795FB5B6C68B2C39822E78BF83B575C918F96E9DB6C36934A79E7D491D5E1", + "size": 2767 + }, + "bin": { + "sha256": "692AA5E70C86522CE2D72B94F372751339A2FEE65F515D06DA5D496D8CF93667", + "size": 1247368 + } + }, + "4xNomos8kSC": { + "name": "4x Nomos8k SC", + "scale": 4, + "license": "CC-BY-4.0", + "author": "Philip Hofmann (Phhofm)", + "description": "High-resolution texture and geometry super-resolution.", + "param": { + "sha256": "4576ED5C2FC5FA250D3C3D585EF02248F26ABDFC1867088078F501FE71E5D61E", + "size": 108039 + }, + "bin": { + "sha256": "DA16E3880D87B177B7C6B659BBD880F8A101B868EB9EBC08D69EAA6D3EDC4517", + "size": 33424520 + } + }, + "4xHFA2k": { + "name": "4x HFA2k", + "scale": 4, + "license": "CC-BY-4.0", + "author": "Philip Hofmann (Phhofm)", + "description": "Illustration and digital art enhancement model.", + "param": { + "sha256": "4576ED5C2FC5FA250D3C3D585EF02248F26ABDFC1867088078F501FE71E5D61E", + "size": 108039 + }, + "bin": { + "sha256": "8A135402B4F39286121B76ABB47601A6B7B7E8D4F3E999A5AAA45ED277824FB4", + "size": 33424520 + } + } + } +} \ No newline at end of file diff --git a/src/Filee.Engines/Upscale/UpscaleModels.cs b/src/Filee.Engines/Upscale/UpscaleModels.cs new file mode 100644 index 00000000..f458926a --- /dev/null +++ b/src/Filee.Engines/Upscale/UpscaleModels.cs @@ -0,0 +1,147 @@ +// Curated image super-resolution models from upscayl/custom-models, pinned in upscale-models.json like the +// engines in engines.json. A model is downloaded when used, checked against its SHA-256 for both .param and .bin, +// and kept in engines/upscale-models// by EngineInstaller. + +using System.Text.Json; +using Filee.Engines.Infrastructure; + +namespace Filee.Engines.Upscale; + +/// One curated image super-resolution model. +public sealed record UpscaleModel( + string Id, + string Name, + int Scale, + string License, + string Author, + string Description, + long ParamSize, + string ParamSha256, + long BinSize, + string BinSha256) +{ + public long TotalSize => ParamSize + BinSize; + public string ParamFileName => $"{Id}.param"; + public string BinFileName => $"{Id}.bin"; +} + +public static class UpscaleModels +{ + public const string Default = "4x_NMKD-Superscale-SP_178000_G"; + + private static readonly (IReadOnlyList All, IReadOnlyDictionary Components) List = Load(); + + /// Every curated model offered. + public static IReadOnlyList All => List.All; + + /// The downloads, for (param and bin per model). + internal static IReadOnlyDictionary Components => List.Components; + + /// Folder that receives one sub folder per model. + public static string Root => Path.Combine(EngineEnvironment.DownloadRoot, "upscale-models"); + + public static UpscaleModel? Find(string id) => All.FirstOrDefault(m => string.Equals(m.Id, id, StringComparison.OrdinalIgnoreCase)); + + /// The folder containing both model files, or null while not installed. + public static string? FolderOf(string id) + { + if (Find(id) is not { } model) + return null; + var folder = Path.Combine(Root, model.Id); + if (!File.Exists(Path.Combine(folder, model.ParamFileName)) || !File.Exists(Path.Combine(folder, model.BinFileName))) + return null; + return new EngineInstaller(Http.Value, Root, Components).IsInstalled(PackageOf(model.Id)) ? folder : null; + } + + public static bool IsInstalled(string id) => FolderOf(id) is not null; + + /// All models that have been downloaded and verified. + public static IReadOnlyList InstalledModels => All.Where(m => IsInstalled(m.Id)).ToList(); + + /// Downloads model if not already installed; one download at a time. + public static async Task EnsureAsync(string id, IProgress? progress = null, CancellationToken cancellationToken = default) + { + if (Find(id) is not { } model) + throw new ArgumentException($"Unknown upscale model '{id}'.", nameof(id)); + + await OneAtATime.WaitAsync(cancellationToken); + try + { + if (FolderOf(model.Id) is { } existing) + return existing; + + var installer = new EngineInstaller(Http.Value, Root, Components); + await installer.InstallAsync(PackageOf(model.Id), progress, cancellationToken); + return FolderOf(model.Id) ?? throw new InvalidDataException($"The {model.Id} model files are missing after download."); + } + finally + { + OneAtATime.Release(); + } + } + + /// Removes a downloaded model cleanly. + public static void Uninstall(string id) + { + if (Find(id) is not { } model) + return; + new EngineInstaller(Http.Value, Root, Components).Uninstall(PackageOf(model.Id)); + } + + private static readonly SemaphoreSlim OneAtATime = new(1, 1); + + private static readonly Lazy Http = new(() => EngineInstaller.CreateHttpClient(TimeSpan.FromMinutes(10))); + + private static EnginePackage PackageOf(string id) => + new("upscale-model-" + id, [$"{id}-param", $"{id}-bin"], Components[$"{id}-param"].Size + Components[$"{id}-bin"].Size, ["upscaler"]); + + private static (IReadOnlyList, IReadOnlyDictionary) Load() + { + using var stream = typeof(UpscaleModels).Assembly.GetManifestResourceStream("Filee.Engines.upscale-models.json") + ?? throw new InvalidOperationException("Embedded upscale-models.json is missing."); + using var json = JsonDocument.Parse(stream); + var urlTemplate = json.RootElement.GetProperty("url").GetString()!; + var commit = json.RootElement.GetProperty("commit").GetString()!; + + var models = new List(); + var components = new Dictionary(); + + foreach (var entry in json.RootElement.GetProperty("models").EnumerateObject()) + { + var id = entry.Name; + var name = entry.Value.GetProperty("name").GetString()!; + var scale = entry.Value.GetProperty("scale").GetInt32(); + var license = entry.Value.GetProperty("license").GetString()!; + var author = entry.Value.GetProperty("author").GetString()!; + var description = entry.Value.GetProperty("description").GetString()!; + + var paramEl = entry.Value.GetProperty("param"); + var paramSha256 = paramEl.GetProperty("sha256").GetString()!; + var paramSize = paramEl.GetProperty("size").GetInt64(); + + var binEl = entry.Value.GetProperty("bin"); + var binSha256 = binEl.GetProperty("sha256").GetString()!; + var binSize = binEl.GetProperty("size").GetInt64(); + + models.Add(new UpscaleModel(id, name, scale, license, author, description, paramSize, paramSha256, binSize, binSha256)); + + components[$"{id}-param"] = new EngineComponent( + $"{id}-param", + commit, + string.Format(System.Globalization.CultureInfo.InvariantCulture, urlTemplate, $"{id}.param"), + paramSha256, + paramSize, + "ncnn-param"); + + components[$"{id}-bin"] = new EngineComponent( + $"{id}-bin", + commit, + string.Format(System.Globalization.CultureInfo.InvariantCulture, urlTemplate, $"{id}.bin"), + binSha256, + binSize, + "ncnn-bin"); + } + + return (models, components); + } +} diff --git a/src/Filee.Engines/Upscale/UpscalerConverter.cs b/src/Filee.Engines/Upscale/UpscalerConverter.cs new file mode 100644 index 00000000..56189027 --- /dev/null +++ b/src/Filee.Engines/Upscale/UpscalerConverter.cs @@ -0,0 +1,81 @@ +// Offline AI image upscaler converter powered by Real-ESRGAN and NCNN. + +using Filee.Core.Conversion; +using Filee.Core.Formats; +using Filee.Core.Presets; +using Filee.Engines.Infrastructure; +using Filee.Engines.Magick; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Filee.Engines.Upscale; + +public sealed class UpscalerConverter : IConverter +{ + private static readonly string[] TargetFormats = + [ + "png", "jpg", "webp", "avif", "heic", "ico", "bmp", "tiff" + ]; + + private readonly UpscalerEngine _engine; + private readonly ILogger _logger; + private string? _executable; + + public UpscalerConverter(ILogger? logger = null) + { + _logger = logger ?? NullLogger.Instance; + _engine = new UpscalerEngine(logger as ILogger); + } + + public string Id => "upscaler"; + public string DisplayName => "Upscayl (AI image upscaler)"; + public int MaxParallelism => 1; + + public IReadOnlyList Edges { get; } = + (from source in ImageEncoder.Sources + where FormatRegistry.FindById(source)?.Category == FormatCategory.Image + from target in TargetFormats + select new ConversionEdge(source, target, Cost: 900, DirectOnly: true, Upscale: true)) + .ToList(); + + public static string? LocateExecutable() => + EngineEnvironment.OwnProgram("upscayl-bin", "upscayl-bin.exe") + ?? EngineEnvironment.BundledProgram("upscayl-bin", "upscayl-bin.exe") + ?? EngineEnvironment.SystemProgram("upscayl-bin", "upscayl-bin.exe"); + + public EngineStatus GetStatus() + { + _executable = LocateExecutable(); + if (_executable is null) + return EngineStatus.Unavailable("engine.reason.not_installed"); + + return EngineStatus.Available(_executable, + EngineEnvironment.UsesFileesCopy("upscayl-bin") ? EngineVersions.Component("upscayl-bin") : null); + } + + public async Task> ConvertAsync(ConversionStep step, IProgress? progress, CancellationToken cancellationToken) + { + var targetFormat = step.To; + var outputPath = step.Output.Allocate(targetFormat); + if (outputPath is null) + return []; + + var options = step.Preset.Upscale; + var modelId = string.IsNullOrWhiteSpace(options.Model) ? UpscaleModels.Default : options.Model; + var scale = options.Scale > 0 ? options.Scale : 4; + + await _engine.UpscaleAsync( + step.InputPath, + outputPath, + step.From, + step.To, + modelId, + scale, + step.WorkDirectory, + step.Preset, + progress, + cancellationToken); + + return [outputPath]; + } +} diff --git a/src/Filee.Engines/Upscale/UpscalerEngine.cs b/src/Filee.Engines/Upscale/UpscalerEngine.cs new file mode 100644 index 00000000..177aa8a1 --- /dev/null +++ b/src/Filee.Engines/Upscale/UpscalerEngine.cs @@ -0,0 +1,162 @@ +// Execution engine for offline image upscaling using Real-ESRGAN / NCNN. + +using System.Globalization; +using System.Text.RegularExpressions; +using Filee.Core.Conversion; +using Filee.Core.Presets; +using Filee.Engines.Infrastructure; +using Filee.Engines.Magick; +using ImageMagick; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Filee.Engines.Upscale; + +public sealed partial class UpscalerEngine +{ + private static readonly TimeSpan StallTimeout = TimeSpan.FromMinutes(5); + private readonly ILogger _logger; + + public UpscalerEngine(ILogger? logger = null) + { + _logger = logger ?? NullLogger.Instance; + } + + [GeneratedRegex(@"^(\d+(?:\.\d+)?)\s*%", RegexOptions.Compiled)] + private static partial Regex PercentageRegex(); + + public async Task UpscaleAsync( + string inputPath, + string outputPath, + string fromFormat, + string toFormat, + string modelId, + int scale, + string workDirectory, + Preset preset, + IProgress? progress = null, + CancellationToken cancellationToken = default) + { + var executable = UpscalerConverter.LocateExecutable() + ?? throw new ConversionRefusedException("error.upscale_needs_engine"); + + var modelFolder = await UpscaleModels.EnsureAsync(modelId, progress: null, cancellationToken); + var model = UpscaleModels.Find(modelId) + ?? throw new InvalidOperationException($"Unknown upscale model '{modelId}'."); + + Directory.CreateDirectory(workDirectory); + var tempInput = Path.Combine(workDirectory, $"upscale-in-{Guid.NewGuid():N}.png"); + var tempOutput = Path.Combine(workDirectory, $"upscale-out-{Guid.NewGuid():N}.png"); + + IExifProfile? exif = null; + IColorProfile? icc = null; + + try + { + // Read source picture through ImageReader (camera RAW, layered PSD/XCF, HEIC, LIVP, KRA, etc.) + using (var images = ImageReader.Read(inputPath, fromFormat, preset)) + { + var frames = ImageEncoder.SelectFrames(images, fromFormat, "png"); + if (frames.Count == 0) + throw new InvalidOperationException("No image frames found to upscale."); + + var frame = frames[0]; + frame.AutoOrient(); + + exif = frame.GetExifProfile(); + icc = frame.GetColorProfile(); + if (exif is not null && exif.ThumbnailLength > 0) + exif.RemoveThumbnail(); + + frame.Write(tempInput, MagickFormat.Png); + } + + var scaleValue = Math.Clamp(scale, 2, 4); + var arguments = new List + { + "-i", tempInput, + "-o", tempOutput, + "-m", modelFolder, + "-n", model.Id, + "-s", scaleValue.ToString(CultureInfo.InvariantCulture), + "-f", "png", + "-v" + }; + + void ParseProgress(string line) + { + if (string.IsNullOrWhiteSpace(line)) return; + var match = PercentageRegex().Match(line.Trim()); + if (match.Success && double.TryParse(match.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var pct)) + { + progress?.Report(Math.Clamp(pct / 100.0, 0.0, 1.0)); + } + } + + var result = await ProcessRunner.RunAsync( + executable, + arguments, + Timeout.InfiniteTimeSpan, + cancellationToken, + workingDirectory: workDirectory, + onOutput: ParseProgress, + stallTimeout: StallTimeout); + + AppLocalRuntime.ThrowIfMissing(executable, result.ExitCode); + + if (result.ExitCode is -1 || + result.StandardError.Contains("vulkan", StringComparison.OrdinalIgnoreCase) || + result.StandardError.Contains("vkCreateInstance", StringComparison.OrdinalIgnoreCase) || + result.StandardError.Contains("no gpu device", StringComparison.OrdinalIgnoreCase) || + result.StandardError.Contains("failed to create instance", StringComparison.OrdinalIgnoreCase)) + { + throw new ConversionRefusedException("error.upscale_needs_vulkan"); + } + + if (result.ExitCode != 0 || !File.Exists(tempOutput)) + { + throw new InvalidOperationException( + $"Upscaling failed (exit code {result.ExitCode}):\n{result.StandardError}"); + } + + progress?.Report(1.0); + + var targetDir = Path.GetDirectoryName(outputPath); + if (!string.IsNullOrEmpty(targetDir)) + Directory.CreateDirectory(targetDir); + + using var upscaled = new MagickImage(tempOutput); + if (preset.Image.KeepMetadata) + { + if (exif is not null) + upscaled.SetProfile(exif); + if (icc is not null) + upscaled.SetProfile(icc); + } + else if (preset.Image.KeepColorProfile && icc is not null) + { + upscaled.SetProfile(icc); + } + + ImageEncoder.ApplyOptions(upscaled, preset.Image, toFormat); + ImageEncoder.WriteImage(upscaled, outputPath, toFormat); + } + finally + { + TryDelete(tempInput); + TryDelete(tempOutput); + } + } + + private static void TryDelete(string file) + { + try + { + if (File.Exists(file)) + File.Delete(file); + } + catch (IOException) + { + } + } +} diff --git a/tests/Filee.App.Tests/ToolbarPaletteViewTests.cs b/tests/Filee.App.Tests/ToolbarPaletteViewTests.cs index 74c57427..bf28e6b7 100644 --- a/tests/Filee.App.Tests/ToolbarPaletteViewTests.cs +++ b/tests/Filee.App.Tests/ToolbarPaletteViewTests.cs @@ -119,7 +119,7 @@ public bool DragToDonut(Border chip) /// which a test of the picture tools would otherwise always see. /// private static void WithoutPictureEditTools(PresetAvailabilityTests.Fixture f) => - f.Store.Presets.RemoveAll(p => p.IsAnnotateTool || p.IsBackgroundTool); + f.Store.Presets.RemoveAll(p => p.IsAnnotateTool || p.IsBackgroundTool || p.IsUpscaleTool); private static ToolbarProfile Profile(string id, params string[] extensions) => new() { diff --git a/tests/Filee.Core.Tests/UpscaleRouteTests.cs b/tests/Filee.Core.Tests/UpscaleRouteTests.cs new file mode 100644 index 00000000..1fdf42e2 --- /dev/null +++ b/tests/Filee.Core.Tests/UpscaleRouteTests.cs @@ -0,0 +1,57 @@ +using Filee.Core.Conversion; +using Filee.Core.Presets; +using Xunit; + +namespace Filee.Core.Tests; + +public sealed class UpscaleRouteTests +{ + private sealed class DummyUpscalerConverter : IConverter + { + public string Id => "upscaler"; + public string DisplayName => "Upscayl (AI image upscaler)"; + public int MaxParallelism => 1; + public IReadOnlyList Edges { get; } = + [ + new("jpg", "jpg", Cost: 900, DirectOnly: true, Upscale: true), + new("jpg", "png", Cost: 900, DirectOnly: true, Upscale: true), + new("png", "png", Cost: 900, DirectOnly: true, Upscale: true), + ]; + + public EngineStatus GetStatus() => EngineStatus.Available(); + public Task> ConvertAsync(ConversionStep step, IProgress? progress, CancellationToken cancellationToken) => + Task.FromResult>([]); + } + + [Fact] + public void RoutePlanner_ignores_upscale_edges_during_normal_planning() + { + var planner = new RoutePlanner([new DummyUpscalerConverter()]); + var normalRoute = planner.Plan("jpg", "jpg", upscale: false); + Assert.Null(normalRoute); + } + + [Fact] + public void RoutePlanner_routes_to_upscaler_when_upscale_flag_is_true() + { + var planner = new RoutePlanner([new DummyUpscalerConverter()]); + var upscaleRoute = planner.Plan("jpg", "jpg", upscale: true); + Assert.NotNull(upscaleRoute); + Assert.Equal("upscaler", upscaleRoute.Steps[0].Converter.Id); + Assert.True(upscaleRoute.Steps[0].Upscale); + } + + [Fact] + public void Preset_UpscaleTool_sets_upscale_flag_and_model() + { + var tool = BuiltInData.UpscalePresets()[0]; + var conversion = UpscaleTool.ConversionOf(tool, "png", "4xLSDIRCompactC3", 4); + + Assert.Equal(PresetKind.Convert, conversion.Kind); + Assert.Null(conversion.Tool); + Assert.Equal("png", conversion.TargetFormat); + Assert.True(conversion.Upscale.Enabled); + Assert.Equal("4xLSDIRCompactC3", conversion.Upscale.Model); + Assert.Equal(4, conversion.Upscale.Scale); + } +} diff --git a/tests/Filee.Core.Tests/UpscaleToolTests.cs b/tests/Filee.Core.Tests/UpscaleToolTests.cs new file mode 100644 index 00000000..91757d1b --- /dev/null +++ b/tests/Filee.Core.Tests/UpscaleToolTests.cs @@ -0,0 +1,51 @@ +// "Upscale Quality": the preset in the library (palette only, no donut slot) and how existing libraries get +// it (settings schema 69). + +using Filee.Core.Presets; +using Filee.Core.Profiles; +using Filee.Core.Settings; +using Xunit; + +namespace Filee.Core.Tests; + +public class UpscaleToolTests +{ + private static List Slots(IEnumerable profiles) => + [.. profiles.Select(p => $"{p.Id}: {string.Join(",", p.PresetIds)} | {string.Join(",", p.BackPresetIds ?? [])}")]; + + [Fact] + public void A_new_library_has_the_tool_after_background_and_no_slot() + { + var presets = BuiltInData.CreatePresets(); + var background = presets.FindIndex(p => p.Id == BuiltInData.BackgroundId); + Assert.True(background >= 0); + Assert.Equal(BuiltInData.UpscaleId, presets[background + 1].Id); + + var tool = presets.Single(p => p.Id == BuiltInData.UpscaleId); + Assert.True(tool.IsTool); + Assert.True(tool.IsUpscaleTool); + Assert.Equal(Preset.UpscaleTool, tool.Tool); + Assert.Equal("tool-upscale", tool.Id); + + // Palette only: no default profile gets a slot (#806) + Assert.All(BuiltInData.CreateProfiles(), p => + Assert.DoesNotContain(BuiltInData.UpscaleId, p.PresetIds.Concat(p.BackPresetIds ?? []))); + } + + [Fact] + public void Schema_69_adds_the_tool_to_the_library_and_no_slot() + { + var presets = BuiltInData.CreatePresets().Where(p => p.Id != BuiltInData.UpscaleId).ToList(); + var profiles = BuiltInData.CreateProfiles(); + var slots = Slots(profiles); + + Assert.True(SettingsMigrations.ApplyToLibrary(68, presets, profiles)); + + var after = presets.FindIndex(p => p.Id == BuiltInData.BackgroundId); + if (after < 0) + after = presets.FindIndex(p => p.Id == BuiltInData.AnnotateId); + Assert.Equal(BuiltInData.UpscaleId, presets[after + 1].Id); + Assert.Equal(slots, Slots(profiles)); + Assert.False(SettingsMigrations.ApplyToLibrary(AppSettings.CurrentSchemaVersion, presets, profiles)); + } +} diff --git a/tests/Filee.Engines.Tests/EngineInstallerTests.cs b/tests/Filee.Engines.Tests/EngineInstallerTests.cs index 515f3259..526ef8ca 100644 --- a/tests/Filee.Engines.Tests/EngineInstallerTests.cs +++ b/tests/Filee.Engines.Tests/EngineInstallerTests.cs @@ -606,6 +606,93 @@ public void Tesseract_keeps_only_what_ocr_needs() Directory.GetFiles(root, "*", SearchOption.AllDirectories).Select(f => Path.GetRelativePath(root, f).Replace('\\', '/')).Order(StringComparer.Ordinal)); } + [Fact] + public async Task Model_components_are_copied_and_marked_installed() + { + var modelBytes = "ggml-model-bytes"u8.ToArray(); + var component = Component("whisper-model", "model", modelBytes); + var server = new FakeServer(new() { [component.Url] = modelBytes }); + var installer = Installer(server, component); + var package = new EnginePackage("whisper", ["whisper-model"], modelBytes.Length, ["whisper"]); + + Assert.False(installer.IsInstalled(package)); + await installer.InstallAsync(package, null, TestContext.Current.CancellationToken); + + Assert.True(File.Exists(Path.Combine(_root, "whisper-model", "whisper-model.model"))); + Assert.True(File.Exists(Path.Combine(_root, "whisper-model", ".filee-component"))); + Assert.True(installer.IsInstalled(package)); + Assert.True(installer.IsUpToDate(package)); + } + + [Fact] + public async Task Ncnn_components_write_custom_marker_and_are_reported_installed() + { + var paramBytes = "ncnn-param-bytes"u8.ToArray(); + var component = Component("model-param", "ncnn-param", paramBytes); + var server = new FakeServer(new() { [component.Url] = paramBytes }); + var installer = Installer(server, component); + var package = new EnginePackage("ncnn-test", ["model-param"], paramBytes.Length, ["upscaler"]); + + Assert.False(installer.IsInstalled(package)); + await installer.InstallAsync(package, null, TestContext.Current.CancellationToken); + + Assert.True(File.Exists(Path.Combine(_root, "model", ".model-param"))); + Assert.True(installer.IsInstalled(package)); + } + + [Fact] + public async Task Uninstalling_a_package_leaves_components_shared_with_another_installed_package_intact() + { + var archiveA = Zip(("toolA.exe", "binaryA")); + var compA = Component("toolA", "zip", archiveA); + var archiveB = Zip(("toolB.exe", "binaryB")); + var compB = Component("toolB", "zip", archiveB); + var archiveShared = Zip(("shared.dll", "sharedDll")); + var compShared = Component("shared", "zip", archiveShared); + + var server = new FakeServer(new() + { + [compA.Url] = archiveA, + [compB.Url] = archiveB, + [compShared.Url] = archiveShared, + }); + + var packageA = new EnginePackage("pkgA", ["toolA", "shared"], 1000, ["toolA"]); + var packageB = new EnginePackage("pkgB", ["toolB", "shared"], 1000, ["toolB"]); + + var installer = new EngineInstaller(new HttpClient(server), _root, + new Dictionary + { + ["toolA"] = compA, + ["toolB"] = compB, + ["shared"] = compShared, + }, + packages: [packageA, packageB]); + + await installer.InstallAsync(packageA, null, TestContext.Current.CancellationToken); + await installer.InstallAsync(packageB, null, TestContext.Current.CancellationToken); + + Assert.True(installer.IsInstalled(packageA)); + Assert.True(installer.IsInstalled(packageB)); + Assert.True(Directory.Exists(Path.Combine(_root, "shared"))); + + // Uninstall pkgA: shared component is still used by pkgB, so shared must stay intact. + installer.Uninstall(packageA); + + Assert.False(installer.IsInstalled(packageA)); + Assert.True(installer.IsInstalled(packageB)); + Assert.False(Directory.Exists(Path.Combine(_root, "toolA"))); + Assert.True(Directory.Exists(Path.Combine(_root, "toolB"))); + Assert.True(Directory.Exists(Path.Combine(_root, "shared"))); + + // Uninstall pkgB: now shared is no longer used by any installed package, so it is removed. + installer.Uninstall(packageB); + + Assert.False(installer.IsInstalled(packageB)); + Assert.False(Directory.Exists(Path.Combine(_root, "toolB"))); + Assert.False(Directory.Exists(Path.Combine(_root, "shared"))); + } + private sealed class SyncProgress(Action report) : IProgress { public void Report(EngineInstallProgress value) => report(value); diff --git a/tests/Filee.Engines.Tests/UpscalerTests.cs b/tests/Filee.Engines.Tests/UpscalerTests.cs new file mode 100644 index 00000000..a30a43f0 --- /dev/null +++ b/tests/Filee.Engines.Tests/UpscalerTests.cs @@ -0,0 +1,233 @@ +using System.IO; +using Filee.Core.Presets; +using Filee.Engines.Magick; +using Filee.Engines.Upscale; +using ImageMagick; +using Xunit; + +namespace Filee.Engines.Tests; + +public class UpscalerTests +{ + [Fact] + public void Catalog_contains_curated_models_with_valid_metadata() + { + var models = UpscaleModels.All; + Assert.NotEmpty(models); + Assert.True(models.Count >= 9); + + foreach (var model in models) + { + Assert.False(string.IsNullOrWhiteSpace(model.Id)); + Assert.False(string.IsNullOrWhiteSpace(model.Name)); + Assert.True(model.Scale >= 2); + Assert.False(string.IsNullOrWhiteSpace(model.License)); + Assert.False(string.IsNullOrWhiteSpace(model.Author)); + Assert.False(string.IsNullOrWhiteSpace(model.Description)); + Assert.True(model.ParamSize > 0); + Assert.Equal(64, model.ParamSha256.Length); + Assert.True(model.BinSize > 0); + Assert.Equal(64, model.BinSha256.Length); + Assert.EndsWith(".bin", model.BinFileName); + Assert.EndsWith(".param", model.ParamFileName); + } + } + + [Fact] + public void Catalog_contains_only_permissive_and_free_licenses() + { + var models = UpscaleModels.All; + // All models must allow commercial use (no CC-BY-NC) + Assert.All(models, m => + { + Assert.Contains(m.License, new[] { "WTFPL", "BSD-3-Clause", "CC-BY-4.0" }); + Assert.DoesNotContain("NC", m.License); + }); + } + + [Theory] + [InlineData("png", true)] + [InlineData("jpg", true)] + [InlineData("jpeg", true)] + [InlineData("webp", true)] + [InlineData("avif", true)] + [InlineData("heic", true)] + [InlineData("ico", true)] + [InlineData("bmp", true)] + [InlineData("tiff", true)] + [InlineData("pdf", false)] + [InlineData("docx", false)] + [InlineData("txt", false)] + [InlineData("mp4", false)] + [InlineData("xlsx", false)] + public void CanUpscale_validates_image_formats(string format, bool expected) + { + Assert.Equal(expected, UpscaleTool.CanUpscale(format)); + } + + [Fact] + public void TargetFor_handles_same_as_source_and_explicit_targets() + { + Assert.Equal("png", UpscaleTool.TargetFor("png", BuiltInData.SameAsSource)); + Assert.Equal("jpg", UpscaleTool.TargetFor("jpg", BuiltInData.SameAsSource)); + Assert.Equal("webp", UpscaleTool.TargetFor("png", "webp")); + Assert.Equal("jpg", UpscaleTool.TargetFor("png", "jpg")); + } + + [Fact] + public void TargetFor_falls_back_to_png_for_unsupported_writable_formats() + { + // Unsupported writable formats fall back to PNG + Assert.Equal("png", UpscaleTool.TargetFor("gif", BuiltInData.SameAsSource)); + Assert.Equal("png", UpscaleTool.TargetFor("psd", BuiltInData.SameAsSource)); + Assert.Equal("png", UpscaleTool.TargetFor("jxl", BuiltInData.SameAsSource)); + Assert.Equal("png", UpscaleTool.TargetFor("tga", BuiltInData.SameAsSource)); + Assert.Equal("png", UpscaleTool.TargetFor("jp2", BuiltInData.SameAsSource)); + + // Supported target formats preserve their format + Assert.Equal("png", UpscaleTool.TargetFor("png", BuiltInData.SameAsSource)); + Assert.Equal("jpg", UpscaleTool.TargetFor("jpg", BuiltInData.SameAsSource)); + Assert.Equal("webp", UpscaleTool.TargetFor("webp", BuiltInData.SameAsSource)); + Assert.Equal("avif", UpscaleTool.TargetFor("avif", BuiltInData.SameAsSource)); + Assert.Equal("heic", UpscaleTool.TargetFor("heic", BuiltInData.SameAsSource)); + Assert.Equal("bmp", UpscaleTool.TargetFor("bmp", BuiltInData.SameAsSource)); + Assert.Equal("tiff", UpscaleTool.TargetFor("tiff", BuiltInData.SameAsSource)); + Assert.Equal("ico", UpscaleTool.TargetFor("ico", BuiltInData.SameAsSource)); + + // Explicitly chosen targets are respected + Assert.Equal("webp", UpscaleTool.TargetFor("gif", "webp")); + Assert.Equal("jpg", UpscaleTool.TargetFor("psd", "jpg")); + } + + [Fact] + public void AutoOrient_and_profiles_are_applied_correctly_on_portrait_image() + { + // Stored landscape (200x100), but marked with orientation RightTop (6) to be viewed as portrait + var tempPath = Path.Combine(Path.GetTempPath(), $"test-orient-{Guid.NewGuid():N}.jpg"); + try + { + using (var img = new MagickImage(MagickColors.Blue, 200, 100)) + { + var exif = new ExifProfile(); + exif.SetValue(ExifTag.Make, "FileePhone"); + exif.SetValue(ExifTag.Orientation, (ushort)Orientation.RightTop); + img.SetProfile(exif); + img.SetProfile(ColorProfiles.SRGB); + img.Orientation = Orientation.RightTop; + img.Write(tempPath, MagickFormat.Jpeg); + } + + var preset = BuiltInData.UpscalePresets().Single(); + using var images = ImageReader.Read(tempPath, "jpg", preset); + var frames = ImageEncoder.SelectFrames(images, "jpg", "png"); + Assert.NotEmpty(frames); + + var frame = frames[0]; + Assert.Equal((200u, 100u), (frame.Width, frame.Height)); + Assert.Equal(Orientation.RightTop, frame.Orientation); + + frame.AutoOrient(); + + // After AutoOrient, the pixels are physically upright (100x200) and orientation tag is reset + Assert.Equal((100u, 200u), (frame.Width, frame.Height)); + Assert.True(frame.Orientation is Orientation.TopLeft or Orientation.Undefined); + + var extractedExif = frame.GetExifProfile(); + var extractedIcc = frame.GetColorProfile(); + Assert.NotNull(extractedExif); + Assert.Equal("FileePhone", extractedExif.GetValue(ExifTag.Make)?.Value); + Assert.NotNull(extractedIcc); + } + finally + { + if (File.Exists(tempPath)) File.Delete(tempPath); + } + } + + [Fact] + public void ConversionOf_creates_valid_upscale_preset() + { + var tool = BuiltInData.UpscalePresets().Single(); + var preset = UpscaleTool.ConversionOf(tool, "png", "4xLSDIRCompactC3", 4); + + Assert.Equal(PresetKind.Convert, preset.Kind); + Assert.Null(preset.Tool); + Assert.Equal("png", preset.TargetFormat); + Assert.True(preset.Upscale.Enabled); + Assert.Equal("4xLSDIRCompactC3", preset.Upscale.Model); + Assert.Equal(4, preset.Upscale.Scale); + } + + [Fact] + public void UpscalerConverter_metadata_and_edges() + { + var converter = new UpscalerConverter(); + Assert.Equal("upscaler", converter.Id); + Assert.Equal("Upscayl (AI image upscaler)", converter.DisplayName); + Assert.Equal(1, converter.MaxParallelism); + Assert.NotEmpty(converter.Edges); + + // Edges exist for common readable formats to all 8 targets + foreach (var target in UpscaleTool.Targets) + { + Assert.Contains(converter.Edges, e => e.From == "png" && e.To == target && e.Upscale); + Assert.Contains(converter.Edges, e => e.From == "jpg" && e.To == target && e.Upscale); + Assert.Contains(converter.Edges, e => e.From == "gif" && e.To == target && e.Upscale); + Assert.Contains(converter.Edges, e => e.From == "psd" && e.To == target && e.Upscale); + } + + var status = converter.GetStatus(); + Assert.NotNull(status); + } + + [Fact] + public async Task RealUpscale_processes_image_when_binary_available() + { + var bin = UpscalerConverter.LocateExecutable(); + Assert.SkipUnless(bin is not null && File.Exists(bin), "upscayl-bin is not installed on this PC."); + + var installed = UpscaleModels.InstalledModels; + Assert.SkipUnless(installed.Count > 0, "No upscale model is installed on this PC."); + + var engine = new UpscalerEngine(); + var tempIn = Path.Combine(Path.GetTempPath(), $"test-in-{Guid.NewGuid():N}.png"); + var tempOut = Path.Combine(Path.GetTempPath(), $"test-out-{Guid.NewGuid():N}.png"); + var workDir = Path.Combine(Path.GetTempPath(), $"test-work-{Guid.NewGuid():N}"); + + try + { + using (var img = new MagickImage(MagickColors.Red, 64, 64)) + { + img.Write(tempIn); + } + + var progressReports = new List(); + var progress = new Progress(p => progressReports.Add(p)); + var tool = BuiltInData.UpscalePresets().Single(); + + await engine.UpscaleAsync( + tempIn, + tempOut, + "png", + "png", + installed[0].Id, + 2, + workDir, + tool, + progress, + TestContext.Current.CancellationToken); + + Assert.True(File.Exists(tempOut)); + using var result = new MagickImage(tempOut); + Assert.Equal(128u, result.Width); + Assert.Equal(128u, result.Height); + Assert.NotEmpty(progressReports); + } + finally + { + if (File.Exists(tempIn)) File.Delete(tempIn); + if (File.Exists(tempOut)) File.Delete(tempOut); + if (Directory.Exists(workDir)) Directory.Delete(workDir, recursive: true); + } + } +}