Native-surface model (PILLAR 1c): this tool is PURE COMPUTE — transcribe loads the model, runs inference, and builds the typed TranscriptionResult. The cache-check + persistence bookends + the per-call force control live in the generic transcription adapter (cjm-transcription-adapter-interface); the result DTO lives in cjm-capability-primitives; identity is derived from the installed distribution. No get_plugin_metadata, no self.storage.
WhisperLocalPlugin.is_available
def is_available()->bool: # True if Whisper and its dependencies are available
Check if Whisper is available.
WhisperLocalPlugin.prefetch
def prefetch()->None:
CR-4 (SG-19): eagerly load the model so the first execute() doesn’t pay the download/load cost. Idempotent via _load_model’s None-guard.
WhisperLocalPlugin.on_disable
def on_disable()->None:
CR-2: release the GPU model when the operator disables the plugin (the worker stays alive); lazy reload on the next execute after re-enable.
WhisperLocalPlugin.cleanup
def cleanup()->None:
Release resources on unload.
Testing the Plugin
# Basic functionality (stage-8: pure-compute ToolCapability).from cjm_plugin_system.core.capability import ToolCapabilityplugin = WhisperLocalPlugin()assertisinstance(plugin, ToolCapability)print(f"Whisper available: {plugin.is_available()}")print(f"Plugin version: {plugin.version}")print(f"Config class: {plugin.config_class.__name__}")# Native-surface model: pure-compute `transcribe` replaces the fused `execute`;# cache/persist + identity moved out. `name` is derived from the installed dist# at runtime (worker / in-env introspection), so it is NOT exercised in-notebook# where __package__ is unset.asserthasattr(plugin, "transcribe") andnothasattr(plugin, "execute")assertnothasattr(plugin, "supported_formats")print("WhisperLocalPlugin is a pure-compute ToolCapability (transcribe; no execute/cache/storage)")
Whisper available: False
Plugin version: 0.0.37
Config class: WhisperPluginConfig
WhisperLocalPlugin is a pure-compute ToolCapability (transcribe; no execute/cache/storage)
# Test configuration dataclassfrom dataclasses import fieldsprint("Available models:")model_field =next(f for f in fields(WhisperPluginConfig) if f.name =="model")for model in model_field.metadata.get(SCHEMA_ENUM, []):print(f" - {model}")
Available models:
- tiny
- tiny.en
- base
- base.en
- small
- small.en
- medium
- medium.en
- large
- large-v1
- large-v2
- large-v3
# Test configuration validationtest_configs = [ ({"model": "tiny"}, "Valid config"), ({"model": "invalid"}, "Invalid model"), ({"model": "tiny", "temperature": 1.5}, "Temperature out of range"),]for config, description in test_configs:try: test_cfg = dict_to_config(WhisperPluginConfig, config, validate=True)print(f"{description}: Valid=True")exceptValueErroras e:print(f"{description}: Valid=False")print(f" Error: {str(e)[:100]}")
Valid config: Valid=True
Invalid model: Valid=False
Error: model: 'invalid' is not one of ['tiny', 'tiny.en', 'base', 'base.en', 'small', 'small.en', 'medium',
Temperature out of range: Valid=False
Error: temperature: 1.5 is greater than maximum 1.0
# Test initialization and get_current_config (returns dict now)plugin.initialize({"model": "tiny", "device": "cpu"})current_config = plugin.get_current_config()print(f"Current config (dict): {current_config}")print(f"Current model: {current_config['model']}")
# Test get_config_schema for UI generationimport jsonschema = plugin.get_config_schema()print("JSON Schema for WhisperPluginConfig:")print(f" Name: {schema['name']}")print(f" Properties count: {len(schema['properties'])}")print(f" Model field enum: {schema['properties']['model'].get('enum', [])[:3]}...")print(f"\nFull schema (truncated):")print(json.dumps({k: v for k, v inlist(schema['properties'].items())[:3]}, indent=2))
JSON Schema for WhisperPluginConfig:
Name: WhisperPluginConfig
Properties count: 23
Model field enum: ['tiny', 'tiny.en', 'base']...
Full schema (truncated):
{
"model": {
"type": "string",
"title": "Model",
"description": "Whisper model size. Larger models are more accurate but slower.",
"enum": [
"tiny",
"tiny.en",
"base",
"base.en",
"small",
"small.en",
"medium",
"medium.en",
"large",
"large-v1",
"large-v2",
"large-v3"
],
"default": "base"
},
"device": {
"type": "string",
"title": "Device",
"description": "Device for inference (auto will use CUDA if available)",
"enum": [
"auto",
"cpu",
"cuda"
],
"default": "auto"
},
"language": {
"type": [
"string",
"null"
],
"title": "Language",
"description": "Language code (e.g., 'en', 'es', 'fr') or None for auto-detection",
"default": null
}
}
# Test idempotent initialize - model unload on config changeimport logging# Enable logging to see model unload messageslogging.basicConfig(level=logging.INFO)# Initialize with one modelplugin.initialize({"model": "tiny", "device": "cpu"})print(f"Initial config: model={plugin.config.model}")# Re-initialize with same model (no unload should happen)print("\nRe-initializing with same model...")plugin.initialize({"model": "tiny", "device": "cpu"})# Re-initialize with different model (unload should trigger)print("\nRe-initializing with different model...")plugin.initialize({"model": "base", "device": "cpu"})print(f"New config: model={plugin.config.model}")
INFO:__main__.WhisperLocalPlugin:Initialized Whisper plugin with model 'tiny' on device 'cpu'
INFO:__main__.WhisperLocalPlugin:Initialized Whisper plugin with model 'tiny' on device 'cpu'
INFO:__main__.WhisperLocalPlugin:Config change: Model tiny -> base
INFO:__main__.WhisperLocalPlugin:Initialized Whisper plugin with model 'base' on device 'cpu'
Initial config: model=tiny
Re-initializing with same model...
Re-initializing with different model...
New config: model=base