Skip to content

Plugin Specification (Code Assets & Navigation) ​

This page keeps only copy-paste code assets: the Prompt-and-Go template, the standalone-development SDK stub, the 7 complete runnable templates, and the AI implementation checklist.

All prose contracts (directory layout / mod.json field table / type selection / entrypoint / config_schema / base-class signatures / Host SPI / model artifacts / runtime dependencies / UI slots / runtime environment and red lines …) have been split into knowledge cards — start at the Plugin Development Docs hub: one topic per page, every fact written exactly once.

The single source of truth for the contract is the client source code (src/transbox/plugins/, src/transbox/ui/bridge/): if a page disagrees with the source, the source wins. This page and docs/plugin-dev/ are kept in sync.


0. Direct Prompt for AI Agents (Prompt-and-Go Template) ​

Companion reading: paste this page's §1 (SDK stub) plus the matching type card from the Plugin Development Docs into the AI together — the result drops straight into a plugin folder.

If you want Large Language Models (such as ChatGPT, Claude, or DeepSeek) to develop and generate a 100% compliant, ready-to-run TransBox plugin without having access to the TransBox source code, copy and send the following system prompt along with this document to the AI:

text
You are a senior TransBox plugin development engineer. Follow the "TransBox Plugin Specification & Host Contract" strictly to develop a fully compliant plugin without access to the host application's private codebase.

[Goal & Requirements]:
<Describe your desired functionality here, e.g. integrating a cloud ASR/MT/TTS provider, building an interview copilot assistant, creating a specialized glossary, etc.>

[Mandatory Rules & Constraints]:
1. Directory Structure: Output a standalone directory with a valid mod.json at its root. id must be globally unique (format: author.type.name, never use builtin.*);
2. Contract Inheritance: Python plugins must inherit from the corresponding base class in transbox.plugins.base (BaseASRProvider / BaseMTEngineProvider / BaseTTSProvider / BaseAssistantPlugin / BaseInterpretationProvider / BasePipelineMiddleware / BaseToolPlugin) and implement all abstract methods with strictly matching signatures;
3. Thread Safety & Async: Any long-running tasks, network requests, or LLM inferences MUST run asynchronously via self.run_in_background() and emit stream events via self.emit_event(). Never block the Qt main UI thread;
4. Dynamic Settings: Declare schema in mod.json under config_schema and read values in initialize()/update_config() via self._config.get(). Never hardcode private keys or local paths;
5. Self-Contained Locales: If UI strings or options exist, provide localized JSONs inside locales/ (zh-CN.json and en-US.json) prefixed with plugin.<plugin_id>.*. Never modify core application locale files;
6. Design Guidelines: For QML views, use ThemeTokens and PluggableWindowBackground to adapt to dark/light themes. Never use emojis or raw text characters as icons; always use vector SVGs.

Please output:
1. Complete directory tree;
2. Complete mod.json manifest;
3. Complete Python / QML / JSON code (no missing core logic or placeholders);
4. Local testing and Workshop publishing instructions.


1. Standalone Development Stub (For AI & External Dev) ​

Key Convention for AI Agents & External Developers: When a plugin runs inside TransBox, the runtime automatically provides the transbox.plugins.base package. If you are developing in a standalone environment without the host repository (e.g., standard virtual environments, CI linters, or standalone unit tests), use this fallback import pattern or save it as transbox_stub.py:

python
try:
    from transbox.plugins.base import (
        BasePlugin, BaseASRProvider, BaseMTEngineProvider, BaseTTSProvider,
        BaseAssistantPlugin, BaseInterpretationProvider, BasePipelineMiddleware,
        BaseToolPlugin, Transcript, Translation, PipelineContext, ASRCapabilities
    )
    from transbox.plugins.manifest import ModManifest
except ImportError:
    # ── Lightweight self-contained stub for standalone testing & AI coding ──
    from abc import ABC, abstractmethod
    from dataclasses import dataclass, field
    from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple
    import numpy as np

    @dataclass
    class ModManifest:
        id: str = "demo.plugin"
        name: str = "Demo"
        version: str = "1.0.0"
        type: str = "mt"

    @dataclass
    class Transcript:
        text: str
        language: Optional[str] = None
        words: Optional[List[Any]] = None

    @dataclass
    class Translation:
        text: str
        source_prefix: Optional[str] = None
        complete: bool = True

    @dataclass
    class PipelineContext:
        session_id: str = ""
        source: str = "peer"
        source_lang: str = "auto"
        target_lang: str = "zh"

    class BasePlugin(ABC):
        def __init__(self, manifest: Any) -> None:
            self.manifest = manifest
            self.host: Any = None
            self._config: Dict[str, Any] = {}
        @property
        def id(self) -> str: return getattr(self.manifest, "id", "")
        def initialize(self, config: Optional[Dict[str, Any]] = None) -> None:
            self._config = config or {}
        def shutdown(self) -> None: pass
        def update_config(self, new_config: Dict[str, Any]) -> None:
            self._config.update(new_config)
        def run_in_background(self, target: Callable, *args: Any, **kwargs: Any) -> Any:
            import threading
            t = threading.Thread(target=target, args=args, kwargs=kwargs, daemon=True)
            t.start()
            return t
        def emit_event(self, event_name: str, payload: Any = None) -> None: pass

    class BaseMTEngineProvider(BasePlugin):
        @abstractmethod
        def translate(self, text: str, source_lang: str = "en", target_lang: str = "zh", **kwargs: Any) -> Translation: ...

    class BaseASRProvider(BasePlugin):
        @abstractmethod
        def transcribe(self, audio: np.ndarray, sample_rate: int = 16000, language: str = "auto", **kwargs: Any) -> Transcript: ...

    class BaseTTSProvider(BasePlugin):
        supports_voice_clone: bool = False
        supports_speed: bool = False
        def ensure_loaded(self) -> None: ...
        @abstractmethod
        def synthesize(self, text: str, voice_id: str = "", language: str = "zh") -> Tuple[np.ndarray, int]: ...
        def stream_tts(self, text: str, voice_id: str = "", language: str = "zh"): ...
        def list_voices(self) -> List[Dict[str, str]]: ...
        def register_voice(self, voice_name: str, audio_samples: np.ndarray,
                           reference_text: str = "", sample_rate: int = 24000) -> bool: ...
        def remove_voice(self, voice_name: str) -> bool: ...

    class BaseAssistantPlugin(BasePlugin): pass
    class BasePipelineMiddleware(BasePlugin): pass
    class BaseInterpretationProvider(BasePlugin): pass
    class BaseToolPlugin(BasePlugin): pass

2. Minimal Runnable Templates ​

2.1 Custom MT (rename and use as-is) ​

mod.json:

json
{
  "id": "myteam.mt.demo",
  "name": "我的示范翻译引擎",
  "version": "1.0.0",
  "author": "Me",
  "type": "mt",
  "entrypoint": "my_translator.py:MyMTProvider",
  "config_schema": [
    {
      "key": "prefix_tag",
      "label": "译文前缀",
      "type": "string",
      "default": "【Demo】"
    }
  ]
}

my_translator.py:

python
from __future__ import annotations

from typing import Any, Dict, Optional

from transbox.plugins.base import BaseMTEngineProvider, Translation
from transbox.plugins.manifest import ModManifest


class MyMTProvider(BaseMTEngineProvider):
    def __init__(self, manifest: ModManifest) -> None:
        super().__init__(manifest)
        self.prefix = "【Demo】"

    def initialize(self, config: Optional[Dict[str, Any]] = None) -> None:
        super().initialize(config)
        self.prefix = self._config.get("prefix_tag", "【Demo】")

    def translate(
        self,
        text: str,
        source_lang: str = "en",
        target_lang: str = "zh",
        glossary_context: Optional[str] = None,
        target: Optional[str] = None,
        source: Optional[str] = None,
    ) -> Translation:
        tgt = target or target_lang
        clean = (text or "").strip()
        return Translation(text=f"{self.prefix} {clean} → {tgt}", complete=True)

After enabling: Settings → Machine Translation → select this engine in the engine dropdown.

2.2 Middleware ​

python
from transbox.plugins.base import BasePipelineMiddleware, Transcript, Translation


class TextFilterMiddleware(BasePipelineMiddleware):
    @property
    def priority(self) -> int:
        return 50

    def process_transcript(self, transcript: Transcript, context) -> Transcript:
        text = transcript.text.replace("那个...", "")
        return Transcript(text=text, language=transcript.language)

    def process_translation(self, translation: Translation, context) -> Translation:
        text = translation.text.replace("坏词", "***")
        return Translation(text=text, complete=translation.complete)

entrypoint: "filter_middleware.py:TextFilterMiddleware"。

2.3 Custom ASR Engine (Streaming & Chunk Transcription) ​

mod.json:

json
{
  "id": "myteam.asr.demo",
  "name": "Demo ASR Engine",
  "version": "1.0.0",
  "type": "asr",
  "entrypoint": "my_asr.py:MyASRProvider",
  "config_schema": [
    {
      "key": "api_url",
      "label": "ASR Service Endpoint",
      "type": "string",
      "default": "http://127.0.0.1:8000/transcribe"
    }
  ]
}

my_asr.py:

python
from __future__ import annotations
from typing import Any, Callable, Dict, Iterator, Optional, Tuple
import numpy as np

from transbox.plugins.base import ASRCapabilities, BaseASRProvider, Transcript, WordTime
from transbox.plugins.manifest import ModManifest

class MyASRProvider(BaseASRProvider):
    def __init__(self, manifest: ModManifest) -> None:
        super().__init__(manifest)
        self.api_url = "http://127.0.0.1:8000/transcribe"

    def initialize(self, config: Optional[Dict[str, Any]] = None) -> None:
        super().initialize(config)
        self.api_url = self._config.get("api_url", "http://127.0.0.1:8000/transcribe")

    @property
    def capabilities(self) -> ASRCapabilities:
        return ASRCapabilities(
            has_native_vad=True,
            supports_streaming=True,
            supports_word_timestamps=False,
            supports_language_detection=True
        )

    @property
    def is_streaming_capable(self) -> bool:
        return True

    def transcribe(
        self,
        audio: np.ndarray,
        sample_rate: int = 16000,
        language: str = "auto",
        stream_id: str = "default",
        with_alignment: bool = False,
        on_token: Optional[Callable[[str], None]] = None,
    ) -> Transcript:
        # Full-chunk transcription: audio is mono float32 numpy array at 16kHz in range [-1.0, 1.0]
        return Transcript(text="[ASR Demo] Hello World", language="en")

    def stream_open(self, stream_id: str, language: str = "auto") -> Any:
        return True

    def stream_feed(self, stream_id: str, pcm_chunk: np.ndarray) -> str:
        return ""

    def stream_events(self, stream_id: str) -> Iterator[Tuple[str, bool]]:
        yield ("Hello", False)
        yield ("Hello World", True)

    def stream_close(self, stream_id: str) -> str:
        return "Hello World"

2.4 Custom TTS Engine ​

mod.json:

json
{
  "id": "myteam.tts.demo",
  "name": "Demo TTS",
  "version": "1.0.0",
  "type": "tts",
  "entrypoint": "my_tts.py:MyTTSProvider",
  "capabilities": {
    "voices": true,
    "voice_clone": true,
    "voice_remove": true,
    "speed": false
  }
}

With capabilities.voice_clone: true the settings page shows a "Register cloned voice" panel where users can record 1~30s of reference audio right inside the app (or pick an existing file). The host handles the microphone, WAV writing and decoding — you only implement register_voice().

my_tts.py:

python
from __future__ import annotations
from typing import Any, Dict, Iterator, List, Optional, Tuple
import numpy as np
from transbox.plugins.base import BaseTTSProvider

class MyTTSProvider(BaseTTSProvider):
    supports_voice_clone = True     # same as mod.json capabilities, the union wins
    supports_speed = False          # False → host does pitch-preserving stretching for you

    @property
    def sample_rate(self) -> int:
        return 24000

    # ── Lifecycle (lazy: initialize must not start a service, see [Local-service engine contract](./plugin-dev/providers/local-service.md)) ──
    def initialize(self, config=None):
        super().initialize(config)
        info = self.host.inspect_model_artifact(self.manifest.id, "my-tts-weights")
        self._model_dir = info.get("model_dir")

    def ensure_loaded(self, progress_callback=None):
        """Called by the host when the engine is actually about to be used."""
        self.wait_async_init()
        self._start_server_if_needed()

    def check_ready(self) -> Tuple[bool, str, str]:
        if not self._model_dir:
            return False, "model_not_ready", "Model not ready — download it in Settings"
        return True, "", ""          # a stopped service still counts as usable

    # ── Voices ──
    def list_voices(self) -> List[Dict[str, str]]:
        """kind="registered" entries get a "Delete voice" button. Read-only: never start the service."""
        return [
            {"id": "voice_en_male", "name": "Deep Voice (English)", "lang": "en", "kind": "speaker"},
            {"id": "voice_zh_female", "name": "Sweet Voice (Chinese)", "lang": "zh", "kind": "speaker"},
        ]

    def register_voice(self, voice_name, audio_samples, reference_text="", sample_rate=24000) -> bool:
        """audio_samples: float32 mono numpy (1~30s) provided by the host (file or in-app recording)."""
        self._start_server_if_needed()   # explicit user action: starting the service is fine
        return True

    def remove_voice(self, voice_name: str) -> bool:
        self._start_server_if_needed()
        return True

    def synthesize(
        self,
        text: str,
        voice_id: str = "",
        voice: Optional[str] = None,
        language: str = "en",
    ) -> Tuple[np.ndarray, int]:
        self._start_server_if_needed()   # request entry point self-heal (no-op when ready)
        selected = (voice or voice_id or self._config.get("voice") or "voice_en_male")
        duration = 0.5
        t = np.linspace(0, duration, int(self.sample_rate * duration), endpoint=False)
        audio = (0.3 * np.sin(2 * np.pi * 440 * t)).astype(np.float32)
        return audio, self.sample_rate

    def stream_tts(self, text: str, voice_id="", voice=None, language="en") -> Iterator[np.ndarray]:
        """Optional: yield chunks for earlier playback start; otherwise the host falls back to synthesize."""
        yield self.synthesize(text, voice_id, voice, language)[0]

2.5 AI Interview Copilot (Assistant) ​

copilot.py:

python
from __future__ import annotations
import time
from typing import Any, Dict, Optional
from transbox.plugins.base import BaseAssistantPlugin

class CopilotPlugin(BaseAssistantPlugin):
    def handle_action(self, action: str, params: Dict[str, Any]) -> Any:
        if action == "ask":
            self.run_in_background(self._async_solve, params)
            return {"status": "started"}
        return None

    def _async_solve(self, params: Dict[str, Any]) -> None:
        # Retrieve pure source transcripts from host (peer=interviewer, self=user; no machine translation)
        history = self.host.get_recent_conversation(max_turns=3) if self.host else []
        self.emit_event("start", {"title": "Analyzing question..."})
        for chunk in ["Key Points: ", "1. System High Availability; ", "2. Full-duplex streaming."]:
            time.sleep(0.3)
            self.emit_event("chunk", {"delta": chunk})
        self.emit_event("done", {"summary": "Analysis complete"})

2.6 End-to-End Simultaneous Interpretation (Interpretation) ​

my_interpretation.py:

python
from __future__ import annotations
import numpy as np
from transbox.plugins.base import BaseInterpretationProvider

class MySIProvider(BaseInterpretationProvider):
    def start_session(
        self,
        session_id: str = "peer",
        source_lang: str = "zh",
        target_lang: str = "en",
        voice: str | None = None,
        tts_enabled: bool = False,
    ) -> bool:
        self.emit_status("streaming", session_id)
        return True

    def feed_audio(self, session_id: str, pcm_chunk: np.ndarray) -> None:
        self.emit_transcript("Hello", is_final=False, session_id=session_id)
        self.emit_translation("Hello", "你好", is_final=False, session_id=session_id)

    def stop_session(self, session_id: str = "peer") -> None:
        self.emit_status("idle", session_id)

    def is_active(self, session_id: str = "peer") -> bool:
        return True

2.7 Quick Translate Tool with Global Hotkey (Tool) ​

my_tool.py:

python
from __future__ import annotations
from typing import Any, Dict, Optional
from transbox.plugins.base import BaseToolPlugin

class QuickTranslateTool(BaseToolPlugin):
    def initialize(self, config: Optional[Dict[str, Any]] = None) -> None:
        super().initialize(config)
        if self.host:
            self.host.register_plugin_hotkey(self.id, "run", "Alt+Q", name="Quick Translation")

    def action_run(self, **kwargs) -> Dict[str, Any]:
        if self.host:
            text = "Welcome to TransBox"
            translated = self.host.translate(text, source="en", target="zh")
            self.host.feed_caption(text=text, translation=translated, source="plugin", tts=True)
            self.host.show_toast(f"Translation: {translated}", level="info")
        return {"ok": True}

    def shutdown(self) -> None:
        if self.host:
            self.host.unregister_plugin_hotkeys(self.id)
        super().shutdown()

3. Implementation Checklist for AI Agents ​

Verify each item before implementing:

  • [ ] mod.json contains a non-empty unique id
  • [ ] type ∈
  • [ ] mt plugins declare target_langs in mod.json (including Chinese and English); see Language codes & translatable languages
  • [ ] For offline local models (asr/mt/tts), include local, offline, or gpu in tags to trigger offline hardware management
  • [ ] entrypoint path and class name really exist in the package
  • [ ] Python class inherits the correct base class and implements all abstract methods
  • [ ] If plugin needs i18n, provide locales/zh-CN.json and locales/en-US.json in plugin root; never modify host core source files
  • [ ] If plugin provides UI, use ThemeTokens and PluggableWindowBackground for automatic light/dark adaptation; never use emojis or character glyphs as icons, use clean vector SVGs
  • [ ] Glossary text separator is = or Tab (not ->)
  • [ ] window_backdrop appears only in theme.json
  • [ ] UI slot names use the consumed names from the table below (§15, including copilot_window)
  • [ ] No undocumented third-party packages; no runtime pip
  • [ ] Every config_schema item has both key and label
  • [ ] After dropping into extensions/, the MOD is visible and enableable in the Workshop list


4. Where the Other Sections Went ​

Original sectionNow lives in
§1 Directory Structure · §2 mod.json Field Table · §4 entrypoint RulesLayout & mod.json
§3 Supported type Values (incl. §3.1 caption_extractor)Type overview & selection · Video-site caption extraction
§5.1 Glossary · §5.2 Theme · §5.3/§5.4 i18n · §5.5 toolglossary · theme · i18n · tool
§6 config_schema (incl. §6.1 QML consumption · §6.2 hot reload · §6.3 settings view)config_schema · Custom settings view
§7.1–7.9 Base classes / data types / Host SPIProvider signatures & data types · SPI index & cheat sheet
§7.10 Local offline model artifacts & external runtimeModel artifacts
§7.11 Host runtime dependencies requires_runtimeHost runtimes
§9 Local hot debuggingHot debugging
§10 In-app one-click publishing & updatesPackaging & size limit · Publish channels
§11 Self-contained plugin locales specificationi18n
§12 Runtime environment & hard rulesRuntime environment & red lines
§13 Official example indexExample library
§15 UI Slots (summary)UI slots · QML slots & HostContext

More development topics (local-service contract, dependency gates, threading contract, release process…) are listed in the Plugin Development Docs.


The core team reserves the right to evolve this specification. Please open a GitHub issue for questions.

TransBox — empowering borderless cross-language communication for everyone