diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index fcd98fd..f24003b 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -14,8 +14,6 @@ jobs: strategy: matrix: include: - - python-version: "3.11" - architecture: "x86" - python-version: "3.13" architecture: "x64" steps: @@ -30,20 +28,17 @@ jobs: run: | pip install --upgrade pip wheel pip install -r requirements.txt - if ("${{ matrix.python-version }}" -eq "3.11") { - pip install -r requirements-libs.txt --target ./addon/globalPlugins/CaptionLocal/libs --platform win32 --only-binary=:all: --no-binary=:none: - pip install miniqinference[cli]==0.1.2 --target ./addon/globalPlugins/CaptionLocal/libs --upgrade --platform win_amd64 --only-binary=:all: --no-binary=:none: - } else { - pip install -r requirements-libs.txt --target ./addon/globalPlugins/CaptionLocal/libs - pip install miniqinference[cli]==0.1.2 --target ./addon/globalPlugins/CaptionLocal/libs --upgrade --platform win_amd64 --only-binary=:all: --no-binary=:none: - } + pip install -r requirements-libs.txt --target ./addon/globalPlugins/CaptionLocal/libs --platform win_amd64 --only-binary=:all: --no-binary=:none: + pip install miniqinference[cli]==0.1.2 --target ./addon/globalPlugins/CaptionLocal/libs --upgrade --platform win_amd64 --only-binary=:all: --no-binary=:none: - name: Code checks run: | $env:SKIP="no-commit-to-branch" pre-commit run --all - name: building addon - run: scons version="${{ github.ref_name }}-${{ matrix.python-version }}-${{ matrix.architecture }}" + run: | + $REF_NAME = "${{ github.ref_name }}".Replace('/', '-') + scons version="${REF_NAME}-${{ matrix.python-version }}-${{ matrix.architecture }}" shell: pwsh - uses: actions/upload-artifact@v4 with: diff --git a/addon/globalPlugins/CaptionLocal/__init__.py b/addon/globalPlugins/CaptionLocal/__init__.py index 5ebe932..37b596f 100644 --- a/addon/globalPlugins/CaptionLocal/__init__.py +++ b/addon/globalPlugins/CaptionLocal/__init__.py @@ -21,6 +21,7 @@ import config import scriptHandler import globalPluginHandler +from contentRecog import recogUi # Add libs directory to path _here = os.path.dirname(__file__) @@ -45,7 +46,8 @@ CONFSPEC = { "modelsDir": f"string(default={_modelsDir})", "currentModel": "string(default=Xenova/vit-gpt2-image-captioning)", - "loadModelWhenInit": "boolean(default=true)" + "loadModelWhenInit": "boolean(default=true)", + "copyToClipboard": "boolean(default=false)" } config.conf.spec['captionLocal'] = CONFSPEC @@ -95,7 +97,7 @@ def terminate(self) -> None: ) def script_runCaption(self, gesture) -> None: """Script to run image captioning on the current navigator object.""" - self.imageDescriber.runCaption(gesture) + recogUi.recognizeNavigatorObject(self.imageDescriber) @scriptHandler.script( # Translators: Description for the release model script diff --git a/addon/globalPlugins/CaptionLocal/captioner/base.py b/addon/globalPlugins/CaptionLocal/captioner/base.py index 604a4fc..1efaef2 100644 --- a/addon/globalPlugins/CaptionLocal/captioner/base.py +++ b/addon/globalPlugins/CaptionLocal/captioner/base.py @@ -7,6 +7,9 @@ from abc import ABC, abstractmethod +from typing import Callable + + class ImageCaptioner(ABC): """Abstract interface for image caption generation. @@ -14,12 +17,18 @@ class ImageCaptioner(ABC): """ @abstractmethod - def generateCaption(self, image: str | bytes, maxLength: int | None = None) -> str: + def generateCaption( + self, + image: str | bytes, + maxLength: int | None = None, + onToken: Callable[[str], None] | None = None, + ) -> str: """ Generate a caption for the given image. :param image: Image file path or binary data. :param maxLength: Optional maximum length for the generated caption. + :param onToken: Optional callback for each generated token (for streaming). :return: The generated image caption as a string. """ pass diff --git a/addon/globalPlugins/CaptionLocal/captioner/qwen.py b/addon/globalPlugins/CaptionLocal/captioner/qwen.py index bd01b81..c2f9b8b 100644 --- a/addon/globalPlugins/CaptionLocal/captioner/qwen.py +++ b/addon/globalPlugins/CaptionLocal/captioner/qwen.py @@ -8,6 +8,7 @@ import subprocess import tempfile import io +from typing import Callable from PIL import Image from logHandler import log from .base import ImageCaptioner @@ -57,11 +58,13 @@ def generateCaption( self, image: str | bytes, maxLength: int | None = None, + onToken: Callable[[str], None] | None = None, ) -> str: """Generate image caption using CLI. :param image: Image file path or binary data. :param maxLength: Optional maximum tokens. + :param onToken: Optional callback for each generated token. """ temp_file_path = None image_path = None @@ -123,17 +126,36 @@ def generateCaption( startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - result = subprocess.check_output( + # Use Popen to allow streaming output + process = subprocess.Popen( cmd, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, encoding="utf-8", startupinfo=startupinfo, + bufsize=1, # Line buffered ) - return result.strip() - except subprocess.CalledProcessError as e: - log.error(f"miniqwen-cli failed with exit code {e.returncode}: {e.output}") - raise Exception(f"CLI error: {e.output}") + + full_text = [] + # Read output character by character (or chunk by chunk) + while True: + char = process.stdout.read(1) + if not char and process.poll() is not None: + break + if char: + full_text.append(char) + if onToken: + onToken(char) + + res_text = "".join(full_text).strip() + if process.returncode != 0: + log.error(f"miniqwen-cli failed with exit code {process.returncode}") + # If we have text, it might be the error message + if res_text: + raise Exception(f"CLI error: {res_text}") + raise Exception(f"CLI error with exit code {process.returncode}") + return res_text except Exception as e: log.exception("Error running miniqwen-cli") raise diff --git a/addon/globalPlugins/CaptionLocal/captioner/vitGpt2.py b/addon/globalPlugins/CaptionLocal/captioner/vitGpt2.py index 4b49711..88b7a48 100644 --- a/addon/globalPlugins/CaptionLocal/captioner/vitGpt2.py +++ b/addon/globalPlugins/CaptionLocal/captioner/vitGpt2.py @@ -9,6 +9,7 @@ import re import io from functools import lru_cache +from typing import Callable import numpy as np from PIL import Image @@ -299,12 +300,14 @@ def _generateWithGreedy( self, encoderHiddenStates: np.ndarray, maxLength: int | None = None, + onToken: Callable[[str], None] | None = None, ) -> str: """Generate text using greedy search. :param encoderHiddenStates: Encoder hidden states. :param maxLength: Maximum generation length. + :param onToken: Optional callback for each generated token. :return: Generated text string. """ if maxLength is None: @@ -341,6 +344,12 @@ def _generateWithGreedy( break generatedTokens.append(nextTokenId) + + if onToken: + # Decode only the last token + token_text = self._decodeTokens([nextTokenId]) + if token_text: + onToken(token_text) # Update past_key_values from outputs if len(decoderOutputs) > 1: @@ -364,11 +373,13 @@ def generateCaption( self, image: str | bytes, maxLength: int | None = None, + onToken: Callable[[str], None] | None = None, ) -> str: """Generate image caption. :param image: Image file path or binary data. :param maxLength: Maximum generation length. + :param onToken: Optional callback for each generated token. :return: Generated image caption. """ # Preprocess image @@ -378,6 +389,6 @@ def generateCaption( encoderHiddenStates = self._encodeImage(imageArray) # Generate text - caption = self._generateWithGreedy(encoderHiddenStates, maxLength) + caption = self._generateWithGreedy(encoderHiddenStates, maxLength, onToken=onToken) return caption diff --git a/addon/globalPlugins/CaptionLocal/imageDescriber.py b/addon/globalPlugins/CaptionLocal/imageDescriber.py index 9ae3efa..401a919 100644 --- a/addon/globalPlugins/CaptionLocal/imageDescriber.py +++ b/addon/globalPlugins/CaptionLocal/imageDescriber.py @@ -14,13 +14,16 @@ import threading from threading import Thread import os +import ctypes import wx import config from logHandler import log import ui import api +import queueHandler +from contentRecog import ContentRecognizer, SimpleTextResult, RecogImageInfo from .captioner import ImageCaptioner from .captioner import imageCaptionerFactory @@ -30,74 +33,25 @@ except: pass -def _screenshotNavigator() -> bytes: - """Capture a screenshot of the current navigator object. - :Return: The captured image data as bytes in JPEG format. - """ - # Get the currently focused object on screen - obj = api.getNavigatorObject() - - # Get the object's position and size information - x, y, width, height = obj.location - - # Create a bitmap with the same size as the object - bmp = wx.Bitmap(width, height) - - # Create a memory device context for drawing operations on the bitmap - mem = wx.MemoryDC(bmp) - - # Copy the specified screen region to the memory bitmap - mem.Blit(0, 0, width, height, wx.ScreenDC(), x, y) - - # Convert the bitmap to an image object for more flexible operations - image = bmp.ConvertToImage() - - # Create a byte stream object to save image data as binary data - body = io.BytesIO() - - # Save the image to the byte stream in JPEG format - image.SaveFile(body, wx.BITMAP_TYPE_JPEG) - - # Read the binary image data from the byte stream - imageData = body.getvalue() - return imageData - - -def _messageCaption(captioner: ImageCaptioner, imageData: bytes) -> None: - """Generate a caption for the given image data. - - :param captioner: The captioner instance to use for generation. - :param imageData: The image data to caption. - """ - try: - description = captioner.generateCaption(image=imageData) - except Exception: - # Translators: error message when an image description cannot be generated - wx.CallAfter(ui.message, _("Failed to generate description")) - log.exception("Failed to generate caption") - else: - wx.CallAfter( - ui.message, - # Translators: Presented when an AI image description has been generated. - # {description} will be replaced with the generated image description. - _("Could be: {description}").format(description=description), - ) - wx.CallAfter(api.copyToClip(text=description, notify=False)) - - -class ImageDescriber: +class ImageDescriber(ContentRecognizer): """module for local image caption functionality. This module provides image captioning using local ONNX models. It can capture screen regions and generate descriptive captions. """ + # Translators: Name of the content recognizer + name = _("Local Image Caption") + def __init__(self) -> None: + super().__init__() self.isModelLoaded = False self.captioner: ImageCaptioner | None = None self.captionThread: Thread | None = None self.loadModelThread: Thread | None = None + self._current_text = "" + self._onResult_callback = None enable = config.conf["captionLocal"]["loadModelWhenInit"] # Load model when initializing @@ -107,41 +61,109 @@ def __init__(self) -> None: def terminate(self): for t in [self.captionThread, self.loadModelThread]: if t is not None and t.is_alive(): - # We can't really join here if we are on the main thread and it might block - # but for an addon terminate it should be fine if it's not too long pass self.captioner = None - def runCaption(self, gesture=None) -> None: - """Script to run image captioning on the current navigator object. + def getResizeFactor(self, width, height): + if width < 100 or height < 100: + return 4 + return 1 - :param gesture: The input gesture that triggered this script. + def recognize(self, pixels: ctypes.Array, imageInfo: RecogImageInfo, onResult): + """Asynchronously recognize content from an image. + + @param pixels: The pixels of the image as a two dimensional array of RGBQUADs. + @param imageInfo: Information about the image for recognition. + @param onResult: A callable which takes a RecognitionResult (or an exception on failure). """ - self._doCaption() - - def _doCaption(self) -> None: - """Real logic to run image captioning on the current navigator object.""" - imageData = _screenshotNavigator() - if not self.isModelLoaded: - # In the addon, we might want to just load it or show a message + # If model is not loaded, we might need to load it. + # But in contentRecog context, we should probably fail or message. ui.message(_("loading model...")) self._loadModel() if not self.isModelLoaded: + onResult(Exception(_("Model not loaded"))) return if self.captionThread is not None and self.captionThread.is_alive(): + # Already running? contentRecog usually handles one at a time. return + self._onResult_callback = onResult + self._current_text = "" + self.captionThread = threading.Thread( - target=_messageCaption, - args=(self.captioner, imageData), + target=self._do_recognize, + args=(pixels, imageInfo), name="RunCaptionThread", ) - # Translators: Message when starting image recognition - ui.message(_("getting image description...")) self.captionThread.start() + def _do_recognize(self, pixels, imageInfo): + from PIL import Image + + width = imageInfo.recogWidth + height = imageInfo.recogHeight + + try: + # Convert pixels (BGRA8) to Image + # pixels is ctypes.Array of RGBQUAD (BGRA) + # PIL "RGBX" handles 4-byte pixels, "BGRX" is what we want for BGRA if we ignore A + image = Image.frombytes("RGBX", (width, height), pixels, "raw", "BGRX") + image = image.convert("RGB") + + buffer = io.BytesIO() + image.save(buffer, format="JPEG") + imageData = buffer.getvalue() + + def on_token(token): + self._current_text += token + self._update_result() + + final_caption = self.captioner.generateCaption( + image=imageData, + onToken=on_token + ) + # Final update to ensure UI is shown and text is correct + if final_caption and not self._current_text: + self._current_text = final_caption + self._update_result() + + # Copy to clipboard at the end if enabled + if config.conf['captionLocal'].get('copyToClipboard', False): + queueHandler.queueFunction(queueHandler.eventQueue, api.copyToClip, text=final_caption, notify=False) + + except Exception as e: + log.exception("Failed to generate caption") + if self._onResult_callback: + self._onResult_callback(e) + + def _update_result(self): + if not self._onResult_callback: + return + + result = SimpleTextResult(self._current_text) + + # If this is a RefreshableRecogResultNVDAObject, we can use its _onResult for updates + onResult = self._onResult_callback + ui_obj = getattr(onResult, "__self__", None) + + if ui_obj and hasattr(ui_obj, "result") and ui_obj.result is not None: + # Subsequent update + if hasattr(ui_obj, "_onResult"): + queueHandler.queueFunction(queueHandler.eventQueue, ui_obj._onResult, result) + else: + queueHandler.queueFunction(queueHandler.eventQueue, onResult, result) + elif self._current_text: + # First result (or not a refreshable object) + # Only show UI if we have text + queueHandler.queueFunction(queueHandler.eventQueue, onResult, result) + + def cancel(self): + """Cancel the recognition in progress.""" + # For now, we don't have a good way to kill the thread/onnx inference safely. + self._onResult_callback = None + def _loadModel(self, localModelDirPath: str | None = None) -> None: """Load the ONNX model for image captioning. @@ -167,16 +189,13 @@ def _loadModel(self, localModelDirPath: str | None = None) -> None: ) except FileNotFoundError: self.isModelLoaded = False - # In the addon, we can use the ModelManager to download ui.message(_("Model not found. Please use Model Manager to download.")) except Exception: self.isModelLoaded = False - # Translators: error message when fail to load model wx.CallAfter(ui.message, _("failed to load image captioner")) log.exception("Failed to load image captioner model") else: self.isModelLoaded = True - # Translators: Message when successfully load the model wx.CallAfter(ui.message, _("image captioning on")) def loadModelInBackground(self, localModelDirPath: str | None = None) -> None: @@ -195,7 +214,6 @@ def _doReleaseModel(self) -> None: if hasattr(self, "captioner") and self.captioner: del self.captioner self.captioner = None - # Translators: Message when image captioning terminates ui.message(_("image captioning off")) self.isModelLoaded = False diff --git a/addon/globalPlugins/CaptionLocal/modelManager.py b/addon/globalPlugins/CaptionLocal/modelManager.py index 76f0e3c..0ca0fad 100644 --- a/addon/globalPlugins/CaptionLocal/modelManager.py +++ b/addon/globalPlugins/CaptionLocal/modelManager.py @@ -82,6 +82,9 @@ def _initUI(self): self.useMirrorCb = wx.CheckBox(modelPanel, label=_("Use Mirror (hf-mirror.com)")) self.useMirrorCb.SetValue(self.useMirror) modelSizer.Add(self.useMirrorCb, 0, wx.ALL, 5) + + self.updateConfigBtn = wx.Button(modelPanel, label=_("Update Models List")) + modelSizer.Add(self.updateConfigBtn, 0, wx.ALL, 5) modelPanel.SetSizer(modelSizer) notebook.AddPage(modelPanel, _("Model Config")) @@ -113,7 +116,36 @@ def _initUI(self): def _bindEvents(self): self.addFileBtn.Bind(wx.EVT_BUTTON, self.onAddFile) self.removeFileBtn.Bind(wx.EVT_BUTTON, self.onRemoveFile) - + self.updateConfigBtn.Bind(wx.EVT_BUTTON, self.onUpdateConfig) + + def onUpdateConfig(self, event): + self.updateConfigBtn.Disable() + threading.Thread(target=self._updateConfigWorker, daemon=True).start() + + def _updateConfigWorker(self): + try: + req = urllib.request.Request(MODELS_CONFIG_URL, headers={'User-Agent': 'Mozilla/5.0'}) + with urllib.request.urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + if "models" in data: + with open(MODELS_CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(data, f, indent='\t', ensure_ascii=False) + wx.CallAfter(self._updateConfigSuccess, data["models"]) + else: + wx.CallAfter(self._updateConfigFail, _("Invalid configuration format.")) + except Exception as e: + wx.CallAfter(self._updateConfigFail, str(e)) + + def _updateConfigSuccess(self, models): + self.updateConfigBtn.Enable() + SoundNotification.playSuccess() + wx.MessageBox(_("Models list updated successfully."), _("Success"), wx.OK | wx.ICON_INFORMATION) + + def _updateConfigFail(self, error): + self.updateConfigBtn.Enable() + SoundNotification.playError() + wx.MessageBox(_("Failed to update models list: {error}").format(error=error), _("Error"), wx.OK | wx.ICON_ERROR) + def onAddFile(self, event): dlg = wx.TextEntryDialog(self, _("Enter file path:"), _("Add File")) _bindEscapeToClose(dlg) @@ -220,8 +252,6 @@ def _initUI(self): self.modelCombo.SetSelection(0) modelSizer.Add(self.modelCombo, 1, wx.ALL | wx.EXPAND, 5) - self.updateConfigBtn = wx.Button(panel, label=_("Update Models List")) - modelSizer.Add(self.updateConfigBtn, 0, wx.ALL, 5) modelBox.Add(modelSizer, 0, wx.ALL | wx.EXPAND, 5) mainSizer.Add(modelBox, 0, wx.ALL | wx.EXPAND, 10) @@ -290,7 +320,6 @@ def _bindEvents(self): self.setActiveBtn.Bind(wx.EVT_BUTTON, self.onSetActive) self.downloadBtn.Bind(wx.EVT_BUTTON, self.onDownload) self.modelCombo.Bind(wx.EVT_COMBOBOX, self.onModelSelect) - self.updateConfigBtn.Bind(wx.EVT_BUTTON, self.onUpdateConfig) self.Bind(wx.EVT_CLOSE, self.onClose) def log(self, message: str): @@ -326,6 +355,13 @@ def onAdvancedSettings(self, event): self.filesToDownload = settings['filesToDownload'] self.resolvePath = settings['resolvePath'] self.useMirror = settings['useMirror'] + + # Reload local config in case it was updated + self._loadLocalConfig() + choices = [m.get("name", m.get("id", "Unknown")) for m in self.modelsConfig] + self.modelCombo.Clear() + self.modelCombo.AppendItems(choices) + self.modelInfoText.SetLabel(_("Model: {modelName}").format(modelName=self.modelName)) self.filesInfoText.SetLabel(_("File Count: {count}").format(count=len(self.filesToDownload))) dlg.Destroy() @@ -337,11 +373,6 @@ def onModelSelect(self, event): self.modelInfoText.SetLabel(_("Model: {modelName}").format(modelName=self.modelName)) self.filesInfoText.SetLabel(_("File Count: {count}").format(count=len(self.filesToDownload))) - def onUpdateConfig(self, event): - self.updateConfigBtn.Disable() - self.statusText.SetLabel(_("Updating models list...")) - threading.Thread(target=self._updateConfigWorker, daemon=True).start() - def onSetActive(self, event): import config from logHandler import log @@ -374,39 +405,6 @@ def onSetActive(self, event): self.log(_("❌ Failed to set active model.")) SoundNotification.playError() - def _updateConfigWorker(self): - try: - req = urllib.request.Request(MODELS_CONFIG_URL, headers={'User-Agent': 'Mozilla/5.0'}) - with urllib.request.urlopen(req, timeout=10) as response: - data = json.loads(response.read().decode('utf-8')) - if "models" in data: - with open(MODELS_CONFIG_FILE, "w", encoding="utf-8") as f: - json.dump(data, f, indent='\t', ensure_ascii=False) - wx.CallAfter(self._updateConfigSuccess, data["models"]) - else: - wx.CallAfter(self._updateConfigFail, _("Invalid configuration format.")) - except Exception as e: - wx.CallAfter(self._updateConfigFail, str(e)) - - def _updateConfigSuccess(self, models): - self.modelsConfig = models - choices = [m.get("name", m.get("id", "Unknown")) for m in self.modelsConfig] - self.modelCombo.Clear() - self.modelCombo.AppendItems(choices) - if choices: - self.modelCombo.SetSelection(0) - self._applyModelConfig(self.modelsConfig[0]) - self.modelInfoText.SetLabel(_("Model: {modelName}").format(modelName=self.modelName)) - self.filesInfoText.SetLabel(_("File Count: {count}").format(count=len(self.filesToDownload))) - self.statusText.SetLabel(_("Models list updated successfully.")) - self.updateConfigBtn.Enable() - SoundNotification.playSuccess() - - def _updateConfigFail(self, error): - self.statusText.SetLabel(_("Failed to update models list: {error}").format(error=error)) - self.updateConfigBtn.Enable() - SoundNotification.playError() - def onDownload(self, event): if self.downloadThread and self.downloadThread.is_alive(): if self.downloader: diff --git a/addon/globalPlugins/CaptionLocal/panel.py b/addon/globalPlugins/CaptionLocal/panel.py index a3fcf24..91f6fc4 100644 --- a/addon/globalPlugins/CaptionLocal/panel.py +++ b/addon/globalPlugins/CaptionLocal/panel.py @@ -53,43 +53,13 @@ def makeSettings(self, settingsSizer: wx.Sizer) -> None: sHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) - # Translators: This is a label for an edit field in the CaptionLocal Settings panel. - modelPathLabel = _("models directory") - - groupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=modelPathLabel) - groupBox = groupSizer.GetStaticBox() - groupHelper = sHelper.addItem(gui.guiHelper.BoxSizerHelper(self, sizer=groupSizer)) - - # Translators: The label of a button to browse for a directory or a file. - browseText = _("Browse...") - # Translators: The title of the dialog presented when browsing for the directory. - dirDialogTitle = _("Select a directory") - - directoryPathHelper = gui.guiHelper.PathSelectionHelper(groupBox, browseText, dirDialogTitle) - directoryEntryControl = groupHelper.addItem(directoryPathHelper) - self.modelPathEdit = directoryEntryControl.pathControl - self.modelPathEdit.Value = config.conf['captionLocal']['modelsDir'] - # Translators: A setting in addon settings dialog. self.loadModelWhenInit = sHelper.addItem(wx.CheckBox(self, label=_("load model when init (may cause high use of memory)"))) self.loadModelWhenInit.SetValue(config.conf['captionLocal']['loadModelWhenInit']) - @staticmethod - def getParameterBound(name: str, boundType: str) -> Optional[int]: - """Get the bound of a parameter in the "ndtt" section of the config. - - Args: - name: The name of the parameter. - boundType: Either "min" or "max". - - Returns: - The bound value if found, None otherwise. - """ - try: - return config.conf.getConfigValidation(("ndtt", name)).kwargs[boundType] - except TypeError: - # For older version of configObj (e.g. used in NVDA 2019.2.1) - return config.conf.getConfigValidationParameter(["ndtt", name], boundType) + # Translators: A setting in addon settings dialog. + self.copyToClipboard = sHelper.addItem(wx.CheckBox(self, label=_("Copy result to clipboard automatically"))) + self.copyToClipboard.SetValue(config.conf['captionLocal'].get('copyToClipboard', False)) def onSave(self) -> None: """Save the configuration settings. @@ -99,7 +69,7 @@ def onSave(self) -> None: """ # Make sure we're operating in the "normal" profile if config.conf.profiles[-1].name is None and len(config.conf.profiles) == 1: - config.conf['captionLocal']['modelsDir'] = self.modelPathEdit.GetValue() config.conf['captionLocal']['loadModelWhenInit'] = self.loadModelWhenInit.GetValue() + config.conf['captionLocal']['copyToClipboard'] = self.copyToClipboard.GetValue() else: - log.debugWarning('No configuration saved for CaptionLocal since the current profile is not the default one.') \ No newline at end of file + log.debugWarning('No configuration saved for CaptionLocal since the current profile is not the default one.') diff --git a/addon/locale/en/LC_MESSAGES/nvda.po b/addon/locale/en/LC_MESSAGES/nvda.po index 3656b38..0412953 100644 --- a/addon/locale/en/LC_MESSAGES/nvda.po +++ b/addon/locale/en/LC_MESSAGES/nvda.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: 'nvda-translations@groups.io'\n" -"POT-Creation-Date: 2026-04-18 17:06+0800\n" +"POT-Creation-Date: 2026-05-11 20:31+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -11,67 +11,61 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +#: addon\globalPlugins\CaptionLocal\__init__.py:76 +#: addon\globalPlugins\CaptionLocal\modelManager.py:206 +msgid "Model Manager" +msgstr "" + #. Translators: Description for the image caption script -#: addon\globalPlugins\CaptionLocal\__init__.py:87 +#: addon\globalPlugins\CaptionLocal\__init__.py:93 msgid "image caption using local model" msgstr "" #. Translators: Category of addon in input gestures. -#: addon\globalPlugins\CaptionLocal\__init__.py:89 -#: addon\globalPlugins\CaptionLocal\__init__.py:100 -#: addon\globalPlugins\CaptionLocal\__init__.py:111 +#: addon\globalPlugins\CaptionLocal\__init__.py:95 +#: addon\globalPlugins\CaptionLocal\__init__.py:106 +#: addon\globalPlugins\CaptionLocal\__init__.py:117 msgid "Caption Local" msgstr "" #. Translators: Description for the release model script -#: addon\globalPlugins\CaptionLocal\__init__.py:98 +#: addon\globalPlugins\CaptionLocal\__init__.py:104 msgid "toggle local model" msgstr "" #. Translators: Description for the open model manager script -#: addon\globalPlugins\CaptionLocal\__init__.py:109 +#: addon\globalPlugins\CaptionLocal\__init__.py:115 msgid "open model manager" msgstr "" -#. Translators: error message when an image description cannot be generated -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:77 -msgid "Failed to generate description" +#. Translators: Name of the content recognizer +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:45 +msgid "Local Image Caption" msgstr "" -#. Translators: Presented when an AI image description has been generated. -#. {description} will be replaced with the generated image description. -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:84 -#, python-brace-format -msgid "Could be: {description}" -msgstr "" - -#. In the addon, we might want to just load it or show a message -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:128 +#. If model is not loaded, we might need to load it. +#. But in contentRecog context, we should probably fail or message. +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:82 msgid "loading model..." msgstr "" -#. Translators: Message when starting image recognition -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:142 -msgid "getting image description..." +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:85 +msgid "Model not loaded" msgstr "" -#. In the addon, we can use the ModelManager to download -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:171 +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:192 msgid "Model not found. Please use Model Manager to download." msgstr "" -#. Translators: error message when fail to load model -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:175 +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:195 msgid "failed to load image captioner" msgstr "" -#. Translators: Message when successfully load the model -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:180 +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:199 msgid "image captioning on" msgstr "" -#. Translators: Message when image captioning terminates -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:199 +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:217 msgid "image captioning off" msgstr "" @@ -91,186 +85,179 @@ msgstr "" msgid "Use Mirror (hf-mirror.com)" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:87 +#: addon\globalPlugins\CaptionLocal\modelManager.py:86 +msgid "Update Models List" +msgstr "" + +#: addon\globalPlugins\CaptionLocal\modelManager.py:90 msgid "Model Config" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:91 +#: addon\globalPlugins\CaptionLocal\modelManager.py:94 msgid "Files to Download:" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:98 -#: addon\globalPlugins\CaptionLocal\modelManager.py:118 +#: addon\globalPlugins\CaptionLocal\modelManager.py:101 +#: addon\globalPlugins\CaptionLocal\modelManager.py:150 msgid "Add File" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:99 +#: addon\globalPlugins\CaptionLocal\modelManager.py:102 msgid "Remove File" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:105 +#: addon\globalPlugins\CaptionLocal\modelManager.py:108 msgid "File List" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:118 -msgid "Enter file path:" +#: addon\globalPlugins\CaptionLocal\modelManager.py:135 +msgid "Invalid configuration format." msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:174 -msgid "Model Manager" +#: addon\globalPlugins\CaptionLocal\modelManager.py:142 +msgid "Models list updated successfully." msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:206 +#: addon\globalPlugins\CaptionLocal\modelManager.py:142 +msgid "Success" +msgstr "" + +#: addon\globalPlugins\CaptionLocal\modelManager.py:147 +#, python-brace-format +msgid "Failed to update models list: {error}" +msgstr "" + +#: addon\globalPlugins\CaptionLocal\modelManager.py:147 +#: addon\globalPlugins\CaptionLocal\modelManager.py:454 +msgid "Error" +msgstr "" + +#: addon\globalPlugins\CaptionLocal\modelManager.py:150 +msgid "Enter file path:" +msgstr "" + +#: addon\globalPlugins\CaptionLocal\modelManager.py:238 msgid "Model Download Manager" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:213 +#: addon\globalPlugins\CaptionLocal\modelManager.py:245 msgid "Model Selection" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:215 +#: addon\globalPlugins\CaptionLocal\modelManager.py:247 msgid "Select Model:" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:223 -msgid "Update Models List" -msgstr "" - -#: addon\globalPlugins\CaptionLocal\modelManager.py:228 +#: addon\globalPlugins\CaptionLocal\modelManager.py:258 msgid "Model Directory Settings" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:230 +#: addon\globalPlugins\CaptionLocal\modelManager.py:260 msgid "Models Directory:" msgstr "" -#. Translators: The label of a button to browse for a directory or a file. -#: addon\globalPlugins\CaptionLocal\modelManager.py:233 -#: addon\globalPlugins\CaptionLocal\panel.py:64 +#: addon\globalPlugins\CaptionLocal\modelManager.py:263 msgid "Browse..." msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:238 +#: addon\globalPlugins\CaptionLocal\modelManager.py:268 msgid "Model Information" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:239 -#: addon\globalPlugins\CaptionLocal\modelManager.py:329 -#: addon\globalPlugins\CaptionLocal\modelManager.py:337 -#: addon\globalPlugins\CaptionLocal\modelManager.py:380 +#: addon\globalPlugins\CaptionLocal\modelManager.py:269 +#: addon\globalPlugins\CaptionLocal\modelManager.py:365 +#: addon\globalPlugins\CaptionLocal\modelManager.py:373 #, python-brace-format msgid "Model: {modelName}" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:241 -#: addon\globalPlugins\CaptionLocal\modelManager.py:330 -#: addon\globalPlugins\CaptionLocal\modelManager.py:338 -#: addon\globalPlugins\CaptionLocal\modelManager.py:381 +#: addon\globalPlugins\CaptionLocal\modelManager.py:271 +#: addon\globalPlugins\CaptionLocal\modelManager.py:366 +#: addon\globalPlugins\CaptionLocal\modelManager.py:374 #, python-brace-format msgid "File Count: {count}" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:246 +#: addon\globalPlugins\CaptionLocal\modelManager.py:276 msgid "Advanced Settings..." msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:247 +#: addon\globalPlugins\CaptionLocal\modelManager.py:277 msgid "Set as Active Model" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:251 -#: addon\globalPlugins\CaptionLocal\modelManager.py:443 +#: addon\globalPlugins\CaptionLocal\modelManager.py:281 +#: addon\globalPlugins\CaptionLocal\modelManager.py:460 msgid "Start Download" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:255 +#: addon\globalPlugins\CaptionLocal\modelManager.py:285 msgid "Ready" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:258 +#: addon\globalPlugins\CaptionLocal\modelManager.py:288 msgid "Download Log" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:312 +#: addon\globalPlugins\CaptionLocal\modelManager.py:341 #, python-brace-format msgid "file: {fileName} progress: {progress:.2f}%" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:315 +#: addon\globalPlugins\CaptionLocal\modelManager.py:344 msgid "Select Download Directory" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:342 -msgid "Updating models list..." -msgstr "" - -#: addon\globalPlugins\CaptionLocal\modelManager.py:351 +#: addon\globalPlugins\CaptionLocal\modelManager.py:401 #, python-brace-format msgid "✅ Active model set to: {modelName}" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:355 +#: addon\globalPlugins\CaptionLocal\modelManager.py:405 msgid "❌ Failed to set active model." msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:368 -msgid "Invalid configuration format." -msgstr "" - -#: addon\globalPlugins\CaptionLocal\modelManager.py:382 -msgid "Models list updated successfully." -msgstr "" - -#: addon\globalPlugins\CaptionLocal\modelManager.py:387 -#, python-brace-format -msgid "Failed to update models list: {error}" -msgstr "" - -#: addon\globalPlugins\CaptionLocal\modelManager.py:395 +#: addon\globalPlugins\CaptionLocal\modelManager.py:412 msgid "Cancelling download..." msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:398 +#: addon\globalPlugins\CaptionLocal\modelManager.py:415 msgid "Cancel Download" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:406 +#: addon\globalPlugins\CaptionLocal\modelManager.py:423 msgid "Downloading..." msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:421 +#: addon\globalPlugins\CaptionLocal\modelManager.py:438 msgid "Download cancelled." msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:422 +#: addon\globalPlugins\CaptionLocal\modelManager.py:439 msgid "Cancelled" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:424 +#: addon\globalPlugins\CaptionLocal\modelManager.py:441 msgid "✅ All files downloaded successfully!" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:425 +#: addon\globalPlugins\CaptionLocal\modelManager.py:442 msgid "Completed" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:432 +#: addon\globalPlugins\CaptionLocal\modelManager.py:449 msgid "❌ Download failed for some files." msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:433 +#: addon\globalPlugins\CaptionLocal\modelManager.py:450 msgid "Failed" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:436 +#: addon\globalPlugins\CaptionLocal\modelManager.py:453 #, python-brace-format msgid "Error: {error}" msgstr "" -#: addon\globalPlugins\CaptionLocal\modelManager.py:437 -msgid "Error" -msgstr "" - #: addon\globalPlugins\CaptionLocal\panel.py:37 #, python-brace-format msgid "" @@ -278,23 +265,18 @@ msgid "" "Please close this dialog, set your config profile to default and try again." msgstr "" -#. Translators: This is a label for an edit field in the CaptionLocal Settings panel. +#. Translators: A setting in addon settings dialog. #: addon\globalPlugins\CaptionLocal\panel.py:57 -msgid "models directory" -msgstr "" - -#. Translators: The title of the dialog presented when browsing for the directory. -#: addon\globalPlugins\CaptionLocal\panel.py:66 -msgid "Select a directory" +msgid "load model when init (may cause high use of memory)" msgstr "" #. Translators: A setting in addon settings dialog. -#: addon\globalPlugins\CaptionLocal\panel.py:74 -msgid "load model when init (may cause high use of memory)" +#: addon\globalPlugins\CaptionLocal\panel.py:61 +msgid "Copy result to clipboard automatically" msgstr "" #. Translators: default prompt for image captioning -#: addon\globalPlugins\CaptionLocal\captioner\qwen.py:26 +#: addon\globalPlugins\CaptionLocal\captioner\qwen.py:29 msgid "Please describe the picture in one sentence" msgstr "" diff --git a/addon/locale/zh_CN/LC_MESSAGES/nvda.po b/addon/locale/zh_CN/LC_MESSAGES/nvda.po index 2752309..c8f12f7 100644 --- a/addon/locale/zh_CN/LC_MESSAGES/nvda.po +++ b/addon/locale/zh_CN/LC_MESSAGES/nvda.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: 'nvda-translations@groups.io'\n" -"POT-Creation-Date: 2026-04-18 17:06+0800\n" +"POT-Creation-Date: 2026-05-11 20:21+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,69 +17,64 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +#: addon\globalPlugins\CaptionLocal\__init__.py:76 +#: addon\globalPlugins\CaptionLocal\modelManager.py:206 +msgid "Model Manager" +msgstr "模型管理器" + #. Translators: Description for the image caption script -#: addon\globalPlugins\CaptionLocal\__init__.py:87 +#: addon\globalPlugins\CaptionLocal\__init__.py:93 msgid "image caption using local model" msgstr "使用本地模型进行图像描述" #. Translators: Category of addon in input gestures. -#: addon\globalPlugins\CaptionLocal\__init__.py:89 -#: addon\globalPlugins\CaptionLocal\__init__.py:100 -#: addon\globalPlugins\CaptionLocal\__init__.py:111 +#: addon\globalPlugins\CaptionLocal\__init__.py:95 +#: addon\globalPlugins\CaptionLocal\__init__.py:106 +#: addon\globalPlugins\CaptionLocal\__init__.py:117 msgid "Caption Local" msgstr "本地图像描述" #. Translators: Description for the release model script -#: addon\globalPlugins\CaptionLocal\__init__.py:98 +#: addon\globalPlugins\CaptionLocal\__init__.py:104 #, fuzzy msgid "toggle local model" msgstr "释放本地模型" #. Translators: Description for the open model manager script -#: addon\globalPlugins\CaptionLocal\__init__.py:109 +#: addon\globalPlugins\CaptionLocal\__init__.py:115 msgid "open model manager" msgstr "打开模型管理器" -#. Translators: error message when an image description cannot be generated -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:77 -msgid "Failed to generate description" -msgstr "生成描述失败" - -#. Translators: Presented when an AI image description has been generated. -#. {description} will be replaced with the generated image description. -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:84 -#, python-brace-format -msgid "Could be: {description}" -msgstr "可能是: {description}" +#. Translators: Name of the content recognizer +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:45 +msgid "Local Image Caption" +msgstr "" -#. In the addon, we might want to just load it or show a message -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:128 +#. If model is not loaded, we might need to load it. +#. But in contentRecog context, we should probably fail or message. +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:82 msgid "loading model..." msgstr "正在加载模型..." -#. Translators: Message when starting image recognition -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:142 -msgid "getting image description..." -msgstr "正在获取图像描述" +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:85 +#, fuzzy +msgid "Model not loaded" +msgstr "模型下载管理器" -#. In the addon, we can use the ModelManager to download -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:171 +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:192 msgid "Model not found. Please use Model Manager to download." msgstr "模型未找到,请使用模型管理器下载" -#. Translators: error message when fail to load model -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:175 +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:195 msgid "failed to load image captioner" msgstr "加载失败" -#. Translators: Message when successfully load the model -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:180 +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:199 #, fuzzy msgid "image captioning on" msgstr "使用本地模型进行图像描述" -#. Translators: Message when image captioning terminates -#: addon\globalPlugins\CaptionLocal\imageDescriber.py:199 +#: addon\globalPlugins\CaptionLocal\imageDescriber.py:217 #, fuzzy msgid "image captioning off" msgstr "使用本地模型进行图像描述" @@ -100,195 +95,187 @@ msgstr "解析路径:" msgid "Use Mirror (hf-mirror.com)" msgstr "使用镜像(hf-mirror.com)" -#: addon\globalPlugins\CaptionLocal\modelManager.py:87 +#: addon\globalPlugins\CaptionLocal\modelManager.py:86 +msgid "Update Models List" +msgstr "更新模型列表" + +#: addon\globalPlugins\CaptionLocal\modelManager.py:90 msgid "Model Config" msgstr "模型设置" -#: addon\globalPlugins\CaptionLocal\modelManager.py:91 +#: addon\globalPlugins\CaptionLocal\modelManager.py:94 msgid "Files to Download:" msgstr "需要下载的文件:" -#: addon\globalPlugins\CaptionLocal\modelManager.py:98 -#: addon\globalPlugins\CaptionLocal\modelManager.py:118 +#: addon\globalPlugins\CaptionLocal\modelManager.py:101 +#: addon\globalPlugins\CaptionLocal\modelManager.py:150 msgid "Add File" msgstr "添加文件" -#: addon\globalPlugins\CaptionLocal\modelManager.py:99 +#: addon\globalPlugins\CaptionLocal\modelManager.py:102 msgid "Remove File" msgstr "移除文件" -#: addon\globalPlugins\CaptionLocal\modelManager.py:105 +#: addon\globalPlugins\CaptionLocal\modelManager.py:108 msgid "File List" msgstr "文件列表" -#: addon\globalPlugins\CaptionLocal\modelManager.py:118 +#: addon\globalPlugins\CaptionLocal\modelManager.py:135 +msgid "Invalid configuration format." +msgstr "无效的设置格式" + +#: addon\globalPlugins\CaptionLocal\modelManager.py:142 +msgid "Models list updated successfully." +msgstr "模型列表更新成功" + +#: addon\globalPlugins\CaptionLocal\modelManager.py:142 +msgid "Success" +msgstr "" + +#: addon\globalPlugins\CaptionLocal\modelManager.py:147 +#, fuzzy, python-brace-format +msgid "Failed to update models list: {error}" +msgstr "初始化路径失败: {error}" + +#: addon\globalPlugins\CaptionLocal\modelManager.py:147 +#: addon\globalPlugins\CaptionLocal\modelManager.py:454 +msgid "Error" +msgstr "错误" + +#: addon\globalPlugins\CaptionLocal\modelManager.py:150 msgid "Enter file path:" msgstr "输入文件路径:" -#: addon\globalPlugins\CaptionLocal\modelManager.py:174 -msgid "Model Manager" -msgstr "模型管理器" - -#: addon\globalPlugins\CaptionLocal\modelManager.py:206 +#: addon\globalPlugins\CaptionLocal\modelManager.py:238 msgid "Model Download Manager" msgstr "模型下载管理器" -#: addon\globalPlugins\CaptionLocal\modelManager.py:213 +#: addon\globalPlugins\CaptionLocal\modelManager.py:245 #, fuzzy msgid "Model Selection" msgstr "模型信息" -#: addon\globalPlugins\CaptionLocal\modelManager.py:215 +#: addon\globalPlugins\CaptionLocal\modelManager.py:247 msgid "Select Model:" msgstr "选择模型" -#: addon\globalPlugins\CaptionLocal\modelManager.py:223 -msgid "Update Models List" -msgstr "更新模型列表" - -#: addon\globalPlugins\CaptionLocal\modelManager.py:228 +#: addon\globalPlugins\CaptionLocal\modelManager.py:258 msgid "Model Directory Settings" msgstr "模型目录设置" -#: addon\globalPlugins\CaptionLocal\modelManager.py:230 +#: addon\globalPlugins\CaptionLocal\modelManager.py:260 #, fuzzy msgid "Models Directory:" msgstr "选择目录" -#. Translators: The label of a button to browse for a directory or a file. -#: addon\globalPlugins\CaptionLocal\modelManager.py:233 -#: addon\globalPlugins\CaptionLocal\panel.py:64 +#: addon\globalPlugins\CaptionLocal\modelManager.py:263 msgid "Browse..." msgstr "浏览" -#: addon\globalPlugins\CaptionLocal\modelManager.py:238 +#: addon\globalPlugins\CaptionLocal\modelManager.py:268 msgid "Model Information" msgstr "模型信息" -#: addon\globalPlugins\CaptionLocal\modelManager.py:239 -#: addon\globalPlugins\CaptionLocal\modelManager.py:329 -#: addon\globalPlugins\CaptionLocal\modelManager.py:337 -#: addon\globalPlugins\CaptionLocal\modelManager.py:380 +#: addon\globalPlugins\CaptionLocal\modelManager.py:269 +#: addon\globalPlugins\CaptionLocal\modelManager.py:365 +#: addon\globalPlugins\CaptionLocal\modelManager.py:373 #, python-brace-format msgid "Model: {modelName}" msgstr "模型: {modelName}" -#: addon\globalPlugins\CaptionLocal\modelManager.py:241 -#: addon\globalPlugins\CaptionLocal\modelManager.py:330 -#: addon\globalPlugins\CaptionLocal\modelManager.py:338 -#: addon\globalPlugins\CaptionLocal\modelManager.py:381 +#: addon\globalPlugins\CaptionLocal\modelManager.py:271 +#: addon\globalPlugins\CaptionLocal\modelManager.py:366 +#: addon\globalPlugins\CaptionLocal\modelManager.py:374 #, python-brace-format msgid "File Count: {count}" msgstr "文件数量: {count}" -#: addon\globalPlugins\CaptionLocal\modelManager.py:246 +#: addon\globalPlugins\CaptionLocal\modelManager.py:276 msgid "Advanced Settings..." msgstr "高级设置..." -#: addon\globalPlugins\CaptionLocal\modelManager.py:247 +#: addon\globalPlugins\CaptionLocal\modelManager.py:277 msgid "Set as Active Model" msgstr "激活该模型" -#: addon\globalPlugins\CaptionLocal\modelManager.py:251 -#: addon\globalPlugins\CaptionLocal\modelManager.py:443 +#: addon\globalPlugins\CaptionLocal\modelManager.py:281 +#: addon\globalPlugins\CaptionLocal\modelManager.py:460 msgid "Start Download" msgstr "开始下载" -#: addon\globalPlugins\CaptionLocal\modelManager.py:255 +#: addon\globalPlugins\CaptionLocal\modelManager.py:285 msgid "Ready" msgstr "准备就绪" -#: addon\globalPlugins\CaptionLocal\modelManager.py:258 +#: addon\globalPlugins\CaptionLocal\modelManager.py:288 msgid "Download Log" msgstr "下载日志" -#: addon\globalPlugins\CaptionLocal\modelManager.py:312 +#: addon\globalPlugins\CaptionLocal\modelManager.py:341 #, python-brace-format msgid "file: {fileName} progress: {progress:.2f}%" msgstr "文件: {fileName} 进度: {progress:.2f}%" -#: addon\globalPlugins\CaptionLocal\modelManager.py:315 +#: addon\globalPlugins\CaptionLocal\modelManager.py:344 msgid "Select Download Directory" msgstr "选择下载目录" -#: addon\globalPlugins\CaptionLocal\modelManager.py:342 -#, fuzzy -msgid "Updating models list..." -msgstr "正在加载模型..." - -#: addon\globalPlugins\CaptionLocal\modelManager.py:351 +#: addon\globalPlugins\CaptionLocal\modelManager.py:401 #, fuzzy, python-brace-format msgid "✅ Active model set to: {modelName}" msgstr "模型: {modelName}" -#: addon\globalPlugins\CaptionLocal\modelManager.py:355 +#: addon\globalPlugins\CaptionLocal\modelManager.py:405 msgid "❌ Failed to set active model." msgstr "激活模型失败" -#: addon\globalPlugins\CaptionLocal\modelManager.py:368 -msgid "Invalid configuration format." -msgstr "无效的设置格式" - -#: addon\globalPlugins\CaptionLocal\modelManager.py:382 -msgid "Models list updated successfully." -msgstr "模型列表更新成功" - -#: addon\globalPlugins\CaptionLocal\modelManager.py:387 -#, fuzzy, python-brace-format -msgid "Failed to update models list: {error}" -msgstr "初始化路径失败: {error}" - -#: addon\globalPlugins\CaptionLocal\modelManager.py:395 +#: addon\globalPlugins\CaptionLocal\modelManager.py:412 #, fuzzy msgid "Cancelling download..." msgstr "开始模型文件下载..." -#: addon\globalPlugins\CaptionLocal\modelManager.py:398 +#: addon\globalPlugins\CaptionLocal\modelManager.py:415 #, fuzzy msgid "Cancel Download" msgstr "开始下载" -#: addon\globalPlugins\CaptionLocal\modelManager.py:406 +#: addon\globalPlugins\CaptionLocal\modelManager.py:423 msgid "Downloading..." msgstr "正在下载..." -#: addon\globalPlugins\CaptionLocal\modelManager.py:421 +#: addon\globalPlugins\CaptionLocal\modelManager.py:438 #, fuzzy msgid "Download cancelled." msgstr "下载失败" -#: addon\globalPlugins\CaptionLocal\modelManager.py:422 +#: addon\globalPlugins\CaptionLocal\modelManager.py:439 #, fuzzy msgid "Cancelled" msgstr "取消" -#: addon\globalPlugins\CaptionLocal\modelManager.py:424 +#: addon\globalPlugins\CaptionLocal\modelManager.py:441 #, fuzzy msgid "✅ All files downloaded successfully!" msgstr "✅ 所有文件下载成功! ({success}/{total})" -#: addon\globalPlugins\CaptionLocal\modelManager.py:425 +#: addon\globalPlugins\CaptionLocal\modelManager.py:442 msgid "Completed" msgstr "已完成" -#: addon\globalPlugins\CaptionLocal\modelManager.py:432 +#: addon\globalPlugins\CaptionLocal\modelManager.py:449 #, fuzzy msgid "❌ Download failed for some files." msgstr "下载失败" -#: addon\globalPlugins\CaptionLocal\modelManager.py:433 +#: addon\globalPlugins\CaptionLocal\modelManager.py:450 msgid "Failed" msgstr "失败" -#: addon\globalPlugins\CaptionLocal\modelManager.py:436 +#: addon\globalPlugins\CaptionLocal\modelManager.py:453 #, fuzzy, python-brace-format msgid "Error: {error}" msgstr "下载过程中出现错误: {error}" -#: addon\globalPlugins\CaptionLocal\modelManager.py:437 -msgid "Error" -msgstr "错误" - #: addon\globalPlugins\CaptionLocal\panel.py:37 #, python-brace-format msgid "" @@ -298,24 +285,18 @@ msgstr "" " 附加组件只能从普通配置文件中进行配置。请关闭此对话框,将您的配置文件设置为默" "认情况,然后重试。" -#. Translators: This is a label for an edit field in the CaptionLocal Settings panel. -#: addon\globalPlugins\CaptionLocal\panel.py:57 -#, fuzzy -msgid "models directory" -msgstr "选择目录" - -#. Translators: The title of the dialog presented when browsing for the directory. -#: addon\globalPlugins\CaptionLocal\panel.py:66 -msgid "Select a directory" -msgstr "选择目录" - #. Translators: A setting in addon settings dialog. -#: addon\globalPlugins\CaptionLocal\panel.py:74 +#: addon\globalPlugins\CaptionLocal\panel.py:57 msgid "load model when init (may cause high use of memory)" msgstr "初始化时加载模型(但可能导致内存大量使用)" +#. Translators: A setting in addon settings dialog. +#: addon\globalPlugins\CaptionLocal\panel.py:61 +msgid "Copy result to clipboard automatically" +msgstr "自动将结果复制到剪贴板" + #. Translators: default prompt for image captioning -#: addon\globalPlugins\CaptionLocal\captioner\qwen.py:26 +#: addon\globalPlugins\CaptionLocal\captioner\qwen.py:29 msgid "Please describe the picture in one sentence" msgstr "请一句话描述图片" @@ -335,6 +316,27 @@ msgid "" "objects browsed by NVDA, support shortcut key recognition. " msgstr "" +#~ msgid "Failed to generate description" +#~ msgstr "生成描述失败" + +#, python-brace-format +#~ msgid "Could be: {description}" +#~ msgstr "可能是: {description}" + +#~ msgid "getting image description..." +#~ msgstr "正在获取图像描述" + +#, fuzzy +#~ msgid "Updating models list..." +#~ msgstr "正在加载模型..." + +#, fuzzy +#~ msgid "models directory" +#~ msgstr "选择目录" + +#~ msgid "Select a directory" +#~ msgstr "选择目录" + #~ msgid "model path" #~ msgstr "模型路径" diff --git a/build.bat b/build.bat index c3a5ccf..080a7d3 100644 --- a/build.bat +++ b/build.bat @@ -1,6 +1,6 @@ pip install --upgrade pip wheel pip install -r requirements.txt -pip install -r requirements-libs.txt --target ./addon/globalPlugins/CaptionLocal/libs --platform win32 --only-binary=:all: --no-binary=:none: --upgrade +pip install -r requirements-libs.txt --target ./addon/globalPlugins/CaptionLocal/libs --platform win_amd64 --only-binary=:all: --no-binary=:none: --upgrade pip install miniqinference[cli] --target ./addon/globalPlugins/CaptionLocal/libs --upgrade set SKIP="no-commit-to-branch" diff --git a/buildVars.py b/buildVars.py index c54a1cb..7fa7886 100644 --- a/buildVars.py +++ b/buildVars.py @@ -25,7 +25,7 @@ def _(arg): # Translators: Long description to be shown for this add-on on add-on information from add-ons manager "addon_description" : _("generate caption for image using local AI models, Describe the image objects browsed by NVDA, support shortcut key recognition. "), # version - "addon_version" : "0.2.0", + "addon_version" : "0.3.0", # Author(s) "addon_author" : "tianze ", # URL for the add-on documentation support @@ -35,7 +35,7 @@ def _(arg): # Documentation file name "addon_docFileName": "readme.html", # Minimum NVDA version supported (e.g. "2018.3.0", minor version is optional) - "addon_minimumNVDAVersion" : "2023.1", + "addon_minimumNVDAVersion" : "2026.1.0", # Last NVDA version supported/tested (e.g. "2018.4.0", ideally more recent than minimum version) "addon_lastTestedNVDAVersion" : "2026.1.0", # Add-on update channel (default is None, denoting stable releases, @@ -59,6 +59,7 @@ def _(arg): pythonSources = [ 'addon/globalPlugins/*/*.py', 'addon/globalPlugins/*/*/*.py', + 'addon/globalPlugins/*.py' ] # Files that contain strings for translation. Usually your python sources