Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
951c761
feat: add unified provider interfaces, protocols, and registration de…
Shackless Apr 9, 2026
f356a54
feat: add STT/TTS/LLM adapter classes to all providers (Tasks 2-13)
Shackless Apr 9, 2026
2c556a0
feat: rename WingmanPro to WingmanSubscription, add STT/TTS/LLM adapters
Shackless Apr 9, 2026
9af3436
milestone: M2 complete — all provider adapters created
Shackless Apr 9, 2026
a29afc7
feat: add ProviderFactory — creates providers from config using decor…
Shackless Apr 9, 2026
62dd245
feat: extract ConversationManager from OpenAiWingman
Shackless Apr 9, 2026
89fd655
feat: extract ConversationCondenser from OpenAiWingman
Shackless Apr 9, 2026
6ea37a4
feat: extract ContextBuilder from OpenAiWingman
Shackless Apr 9, 2026
dc1f8ec
feat: extract ToolExecutor from OpenAiWingman
Shackless Apr 9, 2026
286fa97
milestone: M4 complete — all services extracted (ConversationManager,…
Shackless Apr 9, 2026
e247f57
feat: merge OpenAiWingman into Wingman, wire factory + services
Shackless Apr 9, 2026
d113154
milestone: M5 complete — Wingman merged, factory + services wired, ba…
Shackless Apr 9, 2026
8ab5166
feat: add WingmanContext facade for skill API surface control
Shackless Apr 9, 2026
bf30c00
refactor: skills receive WingmanContext facade instead of raw Wingman
Shackless Apr 9, 2026
06902bd
chore: remove custom wingman class support
Shackless Apr 9, 2026
4e0f099
docs: update skill documentation for unified Wingman + WingmanContext
Shackless Apr 9, 2026
9dfac2f
milestone: M6 complete — skill facade, custom wingman removal, full r…
Shackless Apr 9, 2026
be33a26
fix: unify Parakeet and FasterWhisper CUDA auto-detect
Shackless Apr 10, 2026
c09e3e9
cleanup
Shackless Apr 10, 2026
af2985b
refactor(wingman): extract CommandExecutor service
Shackless Apr 10, 2026
a1f98de
refactor(wingman): extract WingmanMcpManager service
Shackless Apr 10, 2026
f6ef6ef
refactor(wingman): extract WingmanSkillManager service
Shackless Apr 10, 2026
46e5987
refactor(wingman): wire skill context into ConversationManager once
Shackless Apr 10, 2026
c6646d2
refactor(wingman): extract TurnMetrics service
Shackless Apr 10, 2026
6dc98c6
refactor(wingman): extract InstantResponseGenerator service
Shackless Apr 10, 2026
93927c3
refactor(wingman): move tool definitions to their owners
Shackless Apr 10, 2026
cd28a8d
refactor(wingman): cache image-gen subscription instance
Shackless Apr 10, 2026
8e24877
refactor(wingman): move threaded_execution to threading_utils
Shackless Apr 10, 2026
da8a5e9
simpliy code
Shackless Apr 10, 2026
af30289
shorten message
Shackless Apr 13, 2026
4c7abec
add endpoint to manually trigger STT
Shackless Apr 13, 2026
3fefb8c
fix: address Copilot PR review feedback (#387)
Shackless Apr 13, 2026
a5049ce
fix: address second round of Copilot PR review feedback (#387)
Shackless Apr 13, 2026
5e50d6b
fix: address third round of Copilot PR review feedback (#387)
Shackless Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 0 additions & 62 deletions docs/parakeet-issues.md

This file was deleted.

22 changes: 22 additions & 0 deletions providers/edge.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
from os import path
from typing import TYPE_CHECKING
from edge_tts import Communicate
from api.enums import TtsProvider
from api.interface import EdgeTtsConfig, SoundConfig
from providers.interfaces import TtsInterface, tts_provider
from services.audio_player import AudioPlayer
from services.file import get_writable_dir
from services.printr import Printr

if TYPE_CHECKING:
from api.interface import WingmanConfig

RECORDING_PATH = "audio_output"
OUTPUT_FILE: str = "edge_tts.mp3"

Expand Down Expand Up @@ -48,3 +54,19 @@ async def __generate_speech(
await communicate.save(file_path)

return communicate, file_path


@tts_provider(TtsProvider.EDGE_TTS)
class EdgeTts(TtsInterface):
def __init__(self, config: "WingmanConfig"):
self._edge = Edge()
self._config = config

async def play_audio(self, text, sound_config, audio_player, wingman_name):
await self._edge.play_audio(
text=text,
config=self._config.edge_tts,
sound_config=sound_config,
audio_player=audio_player,
wingman_name=wingman_name,
)
25 changes: 23 additions & 2 deletions providers/elevenlabs.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import asyncio
from typing import Callable, Optional
from typing import TYPE_CHECKING, Callable, Optional
import requests
from threading import Event, Thread
import numpy as np
import sounddevice as sd
from elevenlabslib import User, GenerationOptions, PlaybackOptions, SFXOptions
from api.enums import LogType, SoundEffect, WingmanInitializationErrorType
from api.enums import LogType, SoundEffect, TtsProvider, WingmanInitializationErrorType
from api.interface import ElevenlabsConfig, SoundConfig, WingmanInitializationError
from providers.interfaces import TtsInterface, tts_provider
from services.audio_player import AudioPlayer
from services.printr import Printr
from services.sound_effects import get_sound_effects
from services.websocket_user import WebSocketUser

if TYPE_CHECKING:
from api.interface import WingmanConfig


class ElevenLabs:
def __init__(self, api_key: str, wingman_name: str):
Expand Down Expand Up @@ -401,3 +405,20 @@ def get_available_models(self):

def get_subscription_data(self):
return self.user.get_subscription_data()


@tts_provider(TtsProvider.ELEVENLABS)
class ElevenLabsTts(TtsInterface):
def __init__(self, elevenlabs_instance: "ElevenLabs", config: "WingmanConfig"):
self._elevenlabs = elevenlabs_instance
self._config = config

async def play_audio(self, text, sound_config, audio_player, wingman_name):
await self._elevenlabs.play_audio(
text=text,
config=self._config.elevenlabs,
sound_config=sound_config,
audio_player=audio_player,
wingman_name=wingman_name,
stream=self._config.elevenlabs.output_streaming,
)
39 changes: 37 additions & 2 deletions providers/faster_whisper.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
from os import path
import gc
from typing import Optional
from typing import TYPE_CHECKING, Optional
from faster_whisper import WhisperModel
from api.enums import LogType
from api.enums import LogType, SttProvider
from api.interface import (
FasterWhisperSettings,
FasterWhisperTranscript,
FasterWhisperSttConfig,
WingmanInitializationError,
)
from providers.interfaces import SttInterface, Transcript, stt_provider
from services.printr import Printr

if TYPE_CHECKING:
from api.interface import WingmanConfig


class FasterWhisper:
def __init__(self, settings: FasterWhisperSettings):
Expand Down Expand Up @@ -116,3 +120,34 @@ def transcribe(

def validate(self, errors: list[WingmanInitializationError]):
pass


@stt_provider(SttProvider.FASTER_WHISPER)
class FasterWhisperStt(SttInterface):
"""Per-wingman adapter around the shared FasterWhisper singleton."""

def __init__(self, shared: "FasterWhisper", config: "WingmanConfig", wingman_name: str):
self._shared = shared
self._config = config
self._wingman_name = wingman_name

async def transcribe(self, filename: str) -> Transcript | None:
hotwords: list[str] = [self._wingman_name]
default_hotwords = self._config.fasterwhisper.hotwords
if default_hotwords:
hotwords.extend(default_hotwords)
wingman_hotwords = self._config.fasterwhisper.additional_hotwords
if wingman_hotwords:
hotwords.extend(wingman_hotwords)

result = self._shared.transcribe(
config=self._config.fasterwhisper,
filename=filename,
hotwords=list(set(hotwords)),
)
if result is None:
return None
return Transcript(
text=result.text,
language=getattr(result, "language", None),
)
19 changes: 19 additions & 0 deletions providers/google.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import re
from typing import TYPE_CHECKING
from google import genai
from google.genai import types
from openai import APIStatusError, OpenAI
from api.enums import ConversationProvider
from providers.interfaces import LlmInterface, llm_provider
from services.printr import Printr

if TYPE_CHECKING:
from api.interface import WingmanConfig

printr = Printr()


Expand Down Expand Up @@ -139,3 +145,16 @@ def get_available_models(self):
if action == "generateContent":
models.append(model)
return models


@llm_provider(ConversationProvider.GOOGLE)
class GoogleLlm(LlmInterface):
def __init__(self, google_instance: "GoogleGenAI", config: "WingmanConfig"):
self._google = google_instance
self._config = config

async def ask(self, messages, tools=None):
return self._google.ask(
messages=messages, tools=tools,
model=self._config.google.conversation_model,
)
37 changes: 37 additions & 0 deletions providers/hume.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import base64
from os import path
from typing import TYPE_CHECKING
import aiofiles
from hume.client import AsyncHumeClient
from hume.tts import (
PostedUtterance,
PostedUtteranceVoiceWithId,
PostedContextWithGenerationId,
)
from api.enums import TtsProvider
from api.interface import (
HumeConfig,
SoundConfig,
VoiceInfo,
WingmanInitializationError,
)
from providers.interfaces import TtsInterface, tts_provider
from services.audio_player import AudioPlayer
from services.file import get_writable_dir
from services.printr import Printr
from services.secret_keeper import SecretKeeper

if TYPE_CHECKING:
from api.interface import WingmanConfig

RECORDING_PATH = "audio_output"
OUTPUT_FILE: str = "hume.mp3"

Expand Down Expand Up @@ -106,3 +112,34 @@ async def __write_result_to_file(self, base64_encoded_audio: str):
async with aiofiles.open(file_path, "wb") as f:
await f.write(audio_data)
return file_path


@tts_provider(TtsProvider.HUME)
class HumeTts(TtsInterface):
def __init__(self, hume_instance: "Hume", config: "WingmanConfig"):
self._hume = hume_instance
self._config = config

async def play_audio(self, text, sound_config, audio_player, wingman_name):
try:
await self._hume.play_audio(
text=text,
config=self._config.hume,
sound_config=sound_config,
audio_player=audio_player,
wingman_name=wingman_name,
)
except RuntimeError as e:
if "Event loop is closed" in str(e):
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
await self._hume.play_audio(
text=text,
config=self._config.hume,
sound_config=sound_config,
audio_player=audio_player,
wingman_name=wingman_name,
)
else:
raise
Loading
Loading