Implement Voice Capability Service for Platform Voice Feature Detection

Resolved 💬 2 comments Opened Oct 20, 2025 by sanchitmonga22 Closed Oct 20, 2025

Priority

CRITICAL - Production Blocker

Context

The Voice Capability Service is responsible for detecting which voice features are available on the current platform (TTS engines, STT support, microphone access, audio playback, etc.). The iOS SDK has a comprehensive VoiceCapabilityService that checks platform capabilities. The Kotlin SDK is missing this critical service.

Current State

  • Status: Service does not exist
  • Impact: Cannot determine which voice features are available at runtime
  • iOS Has: Complete capability detection for all voice-related features

iOS Reference Implementation

  • Service: sdk/runanywhere-swift/Sources/RunAnywhere/Capabilities/VoiceCapability/Services/VoiceCapabilityService.swift
  • Models: sdk/runanywhere-swift/Sources/RunAnywhere/Capabilities/VoiceCapability/Models/

iOS Architecture Pattern

public final class VoiceCapabilityService: CapabilityService {
    public func checkCapability() async -> VoiceCapability
    
    public struct VoiceCapability {
        public let ttsAvailable: Bool
        public let sttAvailable: Bool
        public let microphoneAccess: Bool
        public let audioPlaybackAvailable: Bool
        public let availableTTSVoices: [String]
        public let supportedLanguages: [String]
    }
}

Implementation Plan

1. Define Common Interfaces (commonMain)

// commonMain/kotlin/com/runanywhere/sdk/capabilities/voice/VoiceCapabilityService.kt
interface VoiceCapabilityService {
    suspend fun checkCapability(): VoiceCapability
    suspend fun requestMicrophonePermission(): Boolean
    val isVoiceSupported: Boolean
}

// commonMain/kotlin/com/runanywhere/sdk/capabilities/voice/models/VoiceCapability.kt
data class VoiceCapability(
    val ttsAvailable: Boolean,
    val sttAvailable: Boolean,
    val microphoneAccess: Boolean,
    val audioPlaybackAvailable: Boolean,
    val availableTTSVoices: List<String>,
    val supportedLanguages: List<String>,
    val wakeWordSupported: Boolean = false,
    val platform: Platform
)

enum class Platform {
    ANDROID, JVM, IOS, MACOS, LINUX, WINDOWS
}

2. Platform Implementations

Android (androidMain)
// androidMain/kotlin/com/runanywhere/sdk/capabilities/voice/AndroidVoiceCapabilityService.kt
class AndroidVoiceCapabilityService(
    private val context: Context
) : VoiceCapabilityService {
    
    override suspend fun checkCapability(): VoiceCapability {
        val packageManager = context.packageManager
        val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
        
        // Check TTS availability
        val ttsEngines = TextToSpeech.Engine().getDefaultEngine()
        val ttsAvailable = ttsEngines != null
        
        // Check STT availability
        val sttAvailable = SpeechRecognizer.isRecognitionAvailable(context)
        
        // Check microphone permission
        val microphoneAccess = ContextCompat.checkSelfPermission(
            context,
            Manifest.permission.RECORD_AUDIO
        ) == PackageManager.PERMISSION_GRANTED
        
        // Check audio playback
        val audioPlaybackAvailable = packageManager.hasSystemFeature(
            PackageManager.FEATURE_AUDIO_OUTPUT
        )
        
        // Get available TTS voices
        val tts = TextToSpeech(context, null)
        val voices = tts.voices.map { it.name }
        tts.shutdown()
        
        return VoiceCapability(
            ttsAvailable = ttsAvailable,
            sttAvailable = sttAvailable,
            microphoneAccess = microphoneAccess,
            audioPlaybackAvailable = audioPlaybackAvailable,
            availableTTSVoices = voices,
            supportedLanguages = Locale.getAvailableLocales().map { it.language },
            wakeWordSupported = true, // Porcupine available on Android
            platform = Platform.ANDROID
        )
    }
    
    override suspend fun requestMicrophonePermission(): Boolean {
        // Request permission via Activity
        return withContext(Dispatchers.Main) {
            // Implementation requires Activity context
            false
        }
    }
    
    override val isVoiceSupported: Boolean
        get() = true // Android always supports voice
}
JVM (jvmMain)
// jvmMain/kotlin/com/runanywhere/sdk/capabilities/voice/JvmVoiceCapabilityService.kt
class JvmVoiceCapabilityService : VoiceCapabilityService {
    
    override suspend fun checkCapability(): VoiceCapability {
        // Check if MaryTTS or similar is available
        val ttsAvailable = checkTTSLibrary()
        
        // Check if Vosk or CMU Sphinx is available
        val sttAvailable = checkSTTLibrary()
        
        // Check microphone access (Java Sound API)
        val microphoneAccess = checkMicrophoneAccess()
        
        // Audio playback always available via javax.sound
        val audioPlaybackAvailable = true
        
        return VoiceCapability(
            ttsAvailable = ttsAvailable,
            sttAvailable = sttAvailable,
            microphoneAccess = microphoneAccess,
            audioPlaybackAvailable = audioPlaybackAvailable,
            availableTTSVoices = getTTSVoices(),
            supportedLanguages = listOf("en", "es", "fr", "de"),
            wakeWordSupported = checkWakeWordLibrary(),
            platform = detectPlatform()
        )
    }
    
    override suspend fun requestMicrophonePermission(): Boolean {
        // JVM doesn't have runtime permissions
        return checkMicrophoneAccess()
    }
    
    override val isVoiceSupported: Boolean
        get() = true
    
    private fun checkMicrophoneAccess(): Boolean {
        return try {
            val mixer = AudioSystem.getTargetDataLine(null)
            mixer != null
        } catch (e: Exception) {
            false
        }
    }
    
    private fun detectPlatform(): Platform {
        val os = System.getProperty("os.name").lowercase()
        return when {
            os.contains("win") -> Platform.WINDOWS
            os.contains("mac") -> Platform.MACOS
            os.contains("nux") -> Platform.LINUX
            else -> Platform.JVM
        }
    }
}

3. Service Registration

// commonMain/kotlin/com/runanywhere/sdk/foundation/ServiceContainer.kt
class ServiceContainer {
    val voiceCapabilityService: VoiceCapabilityService by lazy {
        createVoiceCapabilityService()
    }
    
    private fun createVoiceCapabilityService(): VoiceCapabilityService {
        // expect/actual for platform-specific implementation
        return platformVoiceCapabilityService()
    }
}

// expect/actual pattern
expect fun platformVoiceCapabilityService(): VoiceCapabilityService

Files to Create/Modify

New Files (commonMain)

  • [ ] capabilities/voice/VoiceCapabilityService.kt
  • [ ] capabilities/voice/models/VoiceCapability.kt
  • [ ] capabilities/voice/models/Platform.kt

New Files (androidMain)

  • [ ] capabilities/voice/AndroidVoiceCapabilityService.kt

New Files (jvmMain)

  • [ ] capabilities/voice/JvmVoiceCapabilityService.kt

Files to Modify

  • [ ] foundation/ServiceContainer.kt (add voice capability service)

Testing Requirements

  • [ ] Unit tests for VoiceCapability data class
  • [ ] Android platform tests (TTS, STT, microphone checks)
  • [ ] JVM platform tests (library detection)
  • [ ] Permission request tests (Android)
  • [ ] Capability caching tests
  • [ ] Error handling tests (missing permissions)

Success Criteria

  • [ ] Service detects all voice capabilities correctly on Android
  • [ ] Service detects all voice capabilities correctly on JVM
  • [ ] Microphone permission requests work on Android
  • [ ] TTS voice enumeration works
  • [ ] Platform detection is accurate
  • [ ] Service is integrated into ServiceContainer
  • [ ] Components can query capabilities before initialization

Estimated Effort

1-2 weeks (80-100 hours)

  • Common interface design: 1-2 days
  • Android implementation: 3-4 days
  • JVM implementation: 3-4 days
  • Testing: 2-3 days
  • Documentation: 1 day

Dependencies

  • Depends on: #147 (privacy-first logging)
  • Blocks: #149 (TTS Component)
  • Blocks: #150 (Wake Word Detection)
  • Blocks: #151 (Voice Agent Component)

Related Issues

  • Part of capability detection infrastructure
  • Required for runtime feature availability checks
  • Aligns with iOS VoiceCapabilityService architecture

🤖 Generated with Claude Code

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗