diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp index 133a65d..59753d0 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp @@ -903,6 +903,61 @@ namespace HyperTwistContractLibraryInternal return Profile; } + FHyperTwistSpeechDevicePermissionWorkflowProfile + MakeDefaultSpeechDevicePermissionWorkflowProfile(const FString& ProfileId) + { + FHyperTwistSpeechDevicePermissionWorkflowProfile Profile; + Profile.DevicePermissionWorkflowProfileId = ProfileId; + Profile.WorkflowKind = TEXT("operator-device-permission-workflow"); + Profile.PermissionContractId = TEXT("device-permission-workflow-v1"); + Profile.SettingsHandoffContractId = TEXT("device-permission-settings-handoff-v1"); + Profile.PermissionRecheckContractId = TEXT("device-permission-recheck-v1"); + Profile.PermissionRequestActionId = TEXT("request-microphone-permission"); + Profile.OpenSettingsActionId = TEXT("open-microphone-privacy-settings"); + Profile.PermissionRecheckActionId = TEXT("recheck-microphone-permission"); + Profile.bSupportsPermissionRequest = true; + Profile.bSupportsSettingsHandoff = true; + Profile.bSupportsPermissionRecheck = true; + + auto AddPanel = [&Profile]( + const TCHAR* PanelId, + const TCHAR* PanelKind, + const TCHAR* AnchorId, + const bool bVisibleByDefault + ) + { + FHyperTwistSpeechShellPanelLayout Panel; + Panel.PanelId = PanelId; + Panel.PanelKind = PanelKind; + Panel.AnchorId = AnchorId; + Panel.bVisibleByDefault = bVisibleByDefault; + Profile.Panels.Add(Panel); + }; + + auto AddAction = [&Profile]( + const TCHAR* ActionId, + const TCHAR* InputBinding, + const TCHAR* SurfaceId + ) + { + FHyperTwistSpeechShellActionBinding ActionBinding; + ActionBinding.ActionId = ActionId; + ActionBinding.InputBinding = InputBinding; + ActionBinding.SurfaceId = SurfaceId; + Profile.ActionBindings.Add(ActionBinding); + }; + + AddPanel(TEXT("permission-workflow-summary-card"), TEXT("permission-workflow-summary-card"), TEXT("top-left"), true); + AddPanel(TEXT("permission-settings-handoff-card"), TEXT("permission-settings-handoff-card"), TEXT("left-stack-below-summary"), true); + AddPanel(TEXT("permission-recheck-card"), TEXT("permission-recheck-card"), TEXT("right-stack-aligned-summary"), true); + + AddAction(TEXT("request-microphone-permission"), TEXT("P"), TEXT("permission-workflow-summary-card")); + AddAction(TEXT("open-microphone-privacy-settings"), TEXT("S"), TEXT("permission-settings-handoff-card")); + AddAction(TEXT("recheck-microphone-permission"), TEXT("R"), TEXT("permission-recheck-card")); + AddAction(TEXT("close-speech-session"), TEXT("Escape"), TEXT("session-root")); + return Profile; + } + FHyperTwistSpeechUsageCostDashboardShellProfile MakeDefaultSpeechUsageCostDashboardShellProfile( const FString& ProfileId ) @@ -2467,6 +2522,11 @@ FHyperTwistSpeechSessionConfig UHyperTwistContractLibrary::MakeSampleSpeechSessi HyperTwistContractLibraryInternal::MakeDefaultCoachCommandMicrophoneShellProfile( Config.MicrophoneShellProfileId ); + Config.DevicePermissionWorkflowProfileId = TEXT("speech-device-permission-workflow-v1"); + Config.DevicePermissionWorkflowProfileDefinition = + HyperTwistContractLibraryInternal::MakeDefaultSpeechDevicePermissionWorkflowProfile( + Config.DevicePermissionWorkflowProfileId + ); Config.ProviderProfileId = TEXT("speech-provider/local-http-sidecar-profile-v1"); Config.ProviderProfileDefinition = HyperTwistContractLibraryInternal::MakeLocalHttpSpeechProviderProfile( @@ -2644,6 +2704,9 @@ FHyperTwistSpeechServiceHealth UHyperTwistContractLibrary::MakeMockSpeechService TEXT("devicePermissionShell"), TEXT("captureRouteReadinessShell"), TEXT("captureRouteRetry"), + TEXT("devicePermissionWorkflow"), + TEXT("devicePermissionSettingsHandoff"), + TEXT("devicePermissionRecheck"), TEXT("streamingPreview"), TEXT("manualTranscriptCommit"), TEXT("batchedTranscribe"), diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistRecognition/HyperTwistHttpSpeechClient.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistRecognition/HyperTwistHttpSpeechClient.cpp index 30d9c8e..19a06d6 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistRecognition/HyperTwistHttpSpeechClient.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistRecognition/HyperTwistHttpSpeechClient.cpp @@ -324,6 +324,9 @@ FHyperTwistSpeechServiceHealth UHyperTwistHttpSpeechClient::GetSpeechServiceHeal TEXT("devicePermissionShell"), TEXT("captureRouteReadinessShell"), TEXT("captureRouteRetry"), + TEXT("devicePermissionWorkflow"), + TEXT("devicePermissionSettingsHandoff"), + TEXT("devicePermissionRecheck"), TEXT("streamingPreview"), TEXT("manualTranscriptCommit"), TEXT("batchedTranscribe"), @@ -413,6 +416,9 @@ FHyperTwistSpeechServiceHealth UHyperTwistHttpSpeechClient::GetSpeechServiceHeal TEXT("devicePermissionShell"), TEXT("captureRouteReadinessShell"), TEXT("captureRouteRetry"), + TEXT("devicePermissionWorkflow"), + TEXT("devicePermissionSettingsHandoff"), + TEXT("devicePermissionRecheck"), TEXT("streamingPreview"), TEXT("manualTranscriptCommit"), TEXT("batchedTranscribe"), @@ -478,6 +484,9 @@ FHyperTwistSpeechServiceHealth UHyperTwistHttpSpeechClient::GetSpeechServiceHeal Health.Capabilities.AddUnique(TEXT("devicePermissionShell")); Health.Capabilities.AddUnique(TEXT("captureRouteReadinessShell")); Health.Capabilities.AddUnique(TEXT("captureRouteRetry")); + Health.Capabilities.AddUnique(TEXT("devicePermissionWorkflow")); + Health.Capabilities.AddUnique(TEXT("devicePermissionSettingsHandoff")); + Health.Capabilities.AddUnique(TEXT("devicePermissionRecheck")); Health.Capabilities.AddUnique(TEXT("streamingPreview")); Health.Capabilities.AddUnique(TEXT("manualTranscriptCommit")); Health.SupportedOrchestrationProfiles.AddUnique(TEXT("python-batch-transcribe-v1")); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistRecognition/HyperTwistMockSpeechClient.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistRecognition/HyperTwistMockSpeechClient.cpp index 8731992..3b808e3 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistRecognition/HyperTwistMockSpeechClient.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistRecognition/HyperTwistMockSpeechClient.cpp @@ -132,6 +132,9 @@ FHyperTwistSpeechServiceHealth UHyperTwistMockSpeechClient::GetSpeechServiceHeal Health.Capabilities.AddUnique(TEXT("devicePermissionShell")); Health.Capabilities.AddUnique(TEXT("captureRouteReadinessShell")); Health.Capabilities.AddUnique(TEXT("captureRouteRetry")); + Health.Capabilities.AddUnique(TEXT("devicePermissionWorkflow")); + Health.Capabilities.AddUnique(TEXT("devicePermissionSettingsHandoff")); + Health.Capabilities.AddUnique(TEXT("devicePermissionRecheck")); Health.Capabilities.AddUnique(TEXT("streamingPreview")); Health.Capabilities.AddUnique(TEXT("manualTranscriptCommit")); Health.Capabilities.AddUnique(TEXT("batchedTranscribe")); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index 646a501..5f78ac8 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -2916,6 +2916,11 @@ namespace HyperTwistTrainingSubsystemInternal const bool bSessionOpen ); + FHyperTwistSpeechDevicePermissionWorkflowState BuildSpeechDevicePermissionWorkflowState( + const FHyperTwistSpeechSessionConfig& SessionConfig, + const FHyperTwistTrainingCompanionSpeechSessionState* PriorState + ); + FHyperTwistSpeechUsageCostDashboardShellState BuildSpeechUsageCostDashboardShellState( const FHyperTwistSpeechSessionConfig& SessionConfig, const FHyperTwistTrainingCompanionSpeechSessionState* PriorState @@ -3004,6 +3009,61 @@ namespace HyperTwistTrainingSubsystemInternal return Profile; } + FHyperTwistSpeechDevicePermissionWorkflowProfile + BuildDefaultSpeechDevicePermissionWorkflowProfile(const FString& ProfileId) + { + FHyperTwistSpeechDevicePermissionWorkflowProfile Profile; + Profile.DevicePermissionWorkflowProfileId = ProfileId; + Profile.WorkflowKind = TEXT("operator-device-permission-workflow"); + Profile.PermissionContractId = TEXT("device-permission-workflow-v1"); + Profile.SettingsHandoffContractId = TEXT("device-permission-settings-handoff-v1"); + Profile.PermissionRecheckContractId = TEXT("device-permission-recheck-v1"); + Profile.PermissionRequestActionId = TEXT("request-microphone-permission"); + Profile.OpenSettingsActionId = TEXT("open-microphone-privacy-settings"); + Profile.PermissionRecheckActionId = TEXT("recheck-microphone-permission"); + Profile.bSupportsPermissionRequest = true; + Profile.bSupportsSettingsHandoff = true; + Profile.bSupportsPermissionRecheck = true; + + auto AddPanel = [&Profile]( + const TCHAR* PanelId, + const TCHAR* PanelKind, + const TCHAR* AnchorId, + const bool bVisibleByDefault + ) + { + FHyperTwistSpeechShellPanelLayout Panel; + Panel.PanelId = PanelId; + Panel.PanelKind = PanelKind; + Panel.AnchorId = AnchorId; + Panel.bVisibleByDefault = bVisibleByDefault; + Profile.Panels.Add(Panel); + }; + + auto AddAction = [&Profile]( + const TCHAR* ActionId, + const TCHAR* InputBinding, + const TCHAR* SurfaceId + ) + { + FHyperTwistSpeechShellActionBinding ActionBinding; + ActionBinding.ActionId = ActionId; + ActionBinding.InputBinding = InputBinding; + ActionBinding.SurfaceId = SurfaceId; + Profile.ActionBindings.Add(ActionBinding); + }; + + AddPanel(TEXT("permission-workflow-summary-card"), TEXT("permission-workflow-summary-card"), TEXT("top-left"), true); + AddPanel(TEXT("permission-settings-handoff-card"), TEXT("permission-settings-handoff-card"), TEXT("left-stack-below-summary"), true); + AddPanel(TEXT("permission-recheck-card"), TEXT("permission-recheck-card"), TEXT("right-stack-aligned-summary"), true); + + AddAction(TEXT("request-microphone-permission"), TEXT("P"), TEXT("permission-workflow-summary-card")); + AddAction(TEXT("open-microphone-privacy-settings"), TEXT("S"), TEXT("permission-settings-handoff-card")); + AddAction(TEXT("recheck-microphone-permission"), TEXT("R"), TEXT("permission-recheck-card")); + AddAction(TEXT("close-speech-session"), TEXT("Escape"), TEXT("session-root")); + return Profile; + } + FHyperTwistSpeechUsageCostDashboardShellProfile BuildDefaultSpeechUsageCostDashboardShellProfile( const FString& ProfileId ) @@ -3382,6 +3442,9 @@ namespace HyperTwistTrainingSubsystemInternal TEXT("devicePermissionShell"), TEXT("captureRouteReadinessShell"), TEXT("captureRouteRetry"), + TEXT("devicePermissionWorkflow"), + TEXT("devicePermissionSettingsHandoff"), + TEXT("devicePermissionRecheck"), TEXT("streamingPreview"), TEXT("manualTranscriptCommit"), TEXT("batchedTranscribe"), @@ -4486,15 +4549,38 @@ namespace HyperTwistTrainingSubsystemInternal Health.Capabilities.AddUnique(TEXT("providerSettlementExceptionShell")); Health.Capabilities.AddUnique(TEXT("providerExternalPortalHandoff")); Health.Capabilities.AddUnique(TEXT("providerSettlementExceptionReview")); + Health.Capabilities.AddUnique(TEXT("devicePermissionWorkflow")); + Health.Capabilities.AddUnique(TEXT("devicePermissionSettingsHandoff")); + Health.Capabilities.AddUnique(TEXT("devicePermissionRecheck")); } - bool IsSpeechPermissionError(const FString& ErrorText) - { - const FString NormalizedError = ErrorText.ToLower(); - return NormalizedError.Contains(TEXT("permission")) - || NormalizedError.Contains(TEXT("not-allowed")) - || NormalizedError.Contains(TEXT("access-denied")); - } +bool IsSpeechPermissionError(const FString& ErrorText) +{ + const FString NormalizedError = ErrorText.ToLower(); + return NormalizedError.Contains(TEXT("permission")) + || NormalizedError.Contains(TEXT("not-allowed")) + || NormalizedError.Contains(TEXT("access-denied")); +} + +bool IsSpeechPermissionSettingsError(const FString& ErrorText) +{ + const FString NormalizedError = ErrorText.ToLower(); + return IsSpeechPermissionError(ErrorText) + && (NormalizedError.Contains(TEXT("denied")) + || NormalizedError.Contains(TEXT("blocked")) + || NormalizedError.Contains(TEXT("settings")) + || NormalizedError.Contains(TEXT("privacy"))); +} + +bool IsSpeechPermissionRecheckError(const FString& ErrorText) +{ + const FString NormalizedError = ErrorText.ToLower(); + return IsSpeechPermissionError(ErrorText) + && (NormalizedError.Contains(TEXT("recheck")) + || NormalizedError.Contains(TEXT("retry-permission")) + || NormalizedError.Contains(TEXT("verify-permission")) + || NormalizedError.Contains(TEXT("pending"))); +} bool IsSpeechRouteError(const FString& ErrorText) { @@ -4574,7 +4660,7 @@ namespace HyperTwistTrainingSubsystemInternal Issue.IssueKind = TEXT("permission-required"); Issue.StatusLine = TEXT("Microphone permission is required before capture can start."); Issue.DetailLine = - TEXT("Grant microphone access and reopen the shell. This packet only exposes shell posture; it does not own the OS permission workflow."); + TEXT("Use the first-party permission workflow to request access, review system settings, and recheck before reopening capture. Native OS grant execution remains outside this packet."); Issue.RecommendedActionId = Profile.PermissionRequestActionId; AddSpeechMicrophoneShellIssue(ShellState, Issue); @@ -4707,6 +4793,197 @@ namespace HyperTwistTrainingSubsystemInternal ); } + void AddSpeechDevicePermissionWorkflowIssue( + FHyperTwistSpeechDevicePermissionWorkflowState& WorkflowState, + const FHyperTwistSpeechDevicePermissionWorkflowIssue& Issue + ) + { + if (!Issue.IsStructurallyValid()) + { + return; + } + + WorkflowState.Issues.Add(Issue); + if (!WorkflowState.ActiveIssue.IsStructurallyValid()) + { + WorkflowState.ActiveIssue = Issue; + } + } + + void ApplySpeechDevicePermissionWorkflowPosture( + const FHyperTwistSpeechDevicePermissionWorkflowProfile& Profile, + const FHyperTwistTrainingCompanionSpeechSessionState& SessionState, + FHyperTwistSpeechDevicePermissionWorkflowState& WorkflowState + ) + { + const FString PermissionError = !SessionState.LastError.IsEmpty() + ? SessionState.LastError + : SessionState.ServiceHealth.LastError; + + WorkflowState.PermissionStateId = SessionState.MicrophoneShellState.IsStructurallyValid() + ? SessionState.MicrophoneShellState.PermissionStateId + : TEXT("permission-granted"); + WorkflowState.WorkflowStateId = TEXT("permission-workflow-cleared"); + WorkflowState.LatestSourceKind = TEXT("permission-state-granted"); + WorkflowState.StatusLine = SessionState.bSessionOpen + ? TEXT("Microphone permission already granted.") + : TEXT("Permission workflow ready when the speech shell opens."); + WorkflowState.DetailLine = SessionState.bSessionOpen + ? TEXT("The first-party permission workflow is clear for the bounded speech shell.") + : TEXT("Open the companion speech session to evaluate live permission posture."); + WorkflowState.LastPermissionError = PermissionError; + WorkflowState.WorkflowEntryCount = 0; + WorkflowState.PermissionRequestEntryCount = 0; + WorkflowState.SettingsHandoffEntryCount = 0; + WorkflowState.PermissionRecheckEntryCount = 0; + WorkflowState.bPermissionGranted = true; + WorkflowState.bWorkflowSummaryReady = true; + WorkflowState.bPermissionWorkflowReady = true; + WorkflowState.bSettingsReviewRecommended = false; + WorkflowState.bPermissionRequestVisible = false; + WorkflowState.bSettingsHandoffVisible = false; + WorkflowState.bPermissionRecheckVisible = false; + WorkflowState.Entries.Reset(); + WorkflowState.Issues.Reset(); + WorkflowState.ActiveIssue = FHyperTwistSpeechDevicePermissionWorkflowIssue(); + WorkflowState.ActiveIssueCount = 0; + + if (!PermissionError.IsEmpty() && IsSpeechPermissionError(PermissionError)) + { + FHyperTwistSpeechDevicePermissionWorkflowEntry Entry; + FHyperTwistSpeechDevicePermissionWorkflowIssue Issue; + + if (IsSpeechPermissionSettingsError(PermissionError)) + { + Entry.EntryId = TEXT("device-permission-settings-review"); + Entry.SourceKind = TEXT("permission-settings-review-required"); + Entry.WorkflowStateId = TEXT("permission-settings-review-required"); + Entry.StatusLine = + TEXT("Microphone permission must be reviewed in system settings."); + Entry.DetailLine = FString::Printf( + TEXT("The speech lane reported '%s'. Review the system privacy or permissions screen, then recheck before reopening capture."), + *PermissionError + ); + Entry.RecommendedActionId = Profile.OpenSettingsActionId; + Entry.bRequiresSettingsHandoff = true; + Entry.bRequiresPermissionRecheck = true; + Entry.bPermissionGranted = false; + + Issue.IssueId = TEXT("device-permission-settings-review"); + Issue.IssueKind = TEXT("permission-settings-review-required"); + Issue.StatusLine = Entry.StatusLine; + Issue.DetailLine = Entry.DetailLine; + Issue.RecommendedActionId = Profile.OpenSettingsActionId; + + WorkflowState.SettingsHandoffEntryCount = 1; + WorkflowState.PermissionRecheckEntryCount = 1; + WorkflowState.bSettingsReviewRecommended = true; + WorkflowState.bSettingsHandoffVisible = Profile.bSupportsSettingsHandoff; + WorkflowState.bPermissionRecheckVisible = Profile.bSupportsPermissionRecheck; + } + else if (IsSpeechPermissionRecheckError(PermissionError)) + { + Entry.EntryId = TEXT("device-permission-recheck"); + Entry.SourceKind = TEXT("permission-recheck-required"); + Entry.WorkflowStateId = TEXT("permission-recheck-required"); + Entry.StatusLine = + TEXT("Microphone permission must be rechecked before capture resumes."); + Entry.DetailLine = FString::Printf( + TEXT("The speech lane reported '%s'. Recheck permission posture before reopening the bounded shell."), + *PermissionError + ); + Entry.RecommendedActionId = Profile.PermissionRecheckActionId; + Entry.bRequiresPermissionRecheck = true; + Entry.bPermissionGranted = false; + + Issue.IssueId = TEXT("device-permission-recheck"); + Issue.IssueKind = TEXT("permission-recheck-required"); + Issue.StatusLine = Entry.StatusLine; + Issue.DetailLine = Entry.DetailLine; + Issue.RecommendedActionId = Profile.PermissionRecheckActionId; + + WorkflowState.PermissionRecheckEntryCount = 1; + WorkflowState.bPermissionRecheckVisible = Profile.bSupportsPermissionRecheck; + } + else + { + Entry.EntryId = TEXT("device-permission-request"); + Entry.SourceKind = TEXT("permission-request-required"); + Entry.WorkflowStateId = TEXT("permission-request-required"); + Entry.StatusLine = + TEXT("Microphone permission must be requested before capture starts."); + Entry.DetailLine = PermissionError.IsEmpty() + ? TEXT("Use the first-party permission workflow to request microphone access before reopening capture.") + : FString::Printf( + TEXT("The speech lane reported '%s'. Request microphone access before reopening capture."), + *PermissionError + ); + Entry.RecommendedActionId = Profile.PermissionRequestActionId; + Entry.bPermissionGranted = false; + + Issue.IssueId = TEXT("device-permission-request"); + Issue.IssueKind = TEXT("permission-request-required"); + Issue.StatusLine = Entry.StatusLine; + Issue.DetailLine = Entry.DetailLine; + Issue.RecommendedActionId = Profile.PermissionRequestActionId; + + WorkflowState.PermissionRequestEntryCount = 1; + WorkflowState.bPermissionRequestVisible = Profile.bSupportsPermissionRequest; + } + + WorkflowState.Entries.Add(Entry); + WorkflowState.WorkflowEntryCount = WorkflowState.Entries.Num(); + WorkflowState.WorkflowStateId = Entry.WorkflowStateId; + WorkflowState.PermissionStateId = TEXT("permission-required"); + WorkflowState.LatestSourceKind = Entry.SourceKind; + WorkflowState.StatusLine = Entry.StatusLine; + WorkflowState.DetailLine = Entry.DetailLine; + WorkflowState.bPermissionGranted = false; + WorkflowState.bPermissionWorkflowReady = false; + AddSpeechDevicePermissionWorkflowIssue(WorkflowState, Issue); + } + + WorkflowState.ActiveIssueCount = WorkflowState.Issues.Num(); + } + + void SynchronizeSpeechDevicePermissionWorkflowState( + FHyperTwistTrainingCompanionSpeechSessionState& SessionState + ) + { + FHyperTwistSpeechSessionConfig EffectiveConfig = SessionState.SessionConfig; + if (EffectiveConfig.DevicePermissionWorkflowProfileId.IsEmpty()) + { + EffectiveConfig.DevicePermissionWorkflowProfileId = + TEXT("speech-device-permission-workflow-v1"); + } + const FHyperTwistSpeechDevicePermissionWorkflowProfile Profile = + EffectiveConfig.DevicePermissionWorkflowProfileDefinition.IsStructurallyValid() + ? EffectiveConfig.DevicePermissionWorkflowProfileDefinition + : BuildDefaultSpeechDevicePermissionWorkflowProfile( + !EffectiveConfig.DevicePermissionWorkflowProfileId.IsEmpty() + ? EffectiveConfig.DevicePermissionWorkflowProfileId + : TEXT("speech-device-permission-workflow-v1") + ); + + if (!SessionState.DevicePermissionWorkflowState.IsStructurallyValid()) + { + SessionState.DevicePermissionWorkflowState = + BuildSpeechDevicePermissionWorkflowState(EffectiveConfig, &SessionState); + } + + FHyperTwistSpeechDevicePermissionWorkflowState& WorkflowState = + SessionState.DevicePermissionWorkflowState; + WorkflowState.DevicePermissionWorkflowProfileId = + Profile.DevicePermissionWorkflowProfileId; + WorkflowState.AvailableActionIds.Reset(); + for (const FHyperTwistSpeechShellActionBinding& ActionBinding : Profile.ActionBindings) + { + WorkflowState.AvailableActionIds.AddUnique(ActionBinding.ActionId); + } + + ApplySpeechDevicePermissionWorkflowPosture(Profile, SessionState, WorkflowState); + } + void AddSpeechUsageCostDashboardIssue( FHyperTwistSpeechUsageCostDashboardShellState& ShellState, const FHyperTwistSpeechUsageCostDashboardIssue& Issue @@ -6513,6 +6790,46 @@ namespace HyperTwistTrainingSubsystemInternal return ShellState; } + FHyperTwistSpeechDevicePermissionWorkflowState BuildSpeechDevicePermissionWorkflowState( + const FHyperTwistSpeechSessionConfig& SessionConfig, + const FHyperTwistTrainingCompanionSpeechSessionState* PriorState + ) + { + const FHyperTwistSpeechDevicePermissionWorkflowProfile Profile = + SessionConfig.DevicePermissionWorkflowProfileDefinition.IsStructurallyValid() + ? SessionConfig.DevicePermissionWorkflowProfileDefinition + : BuildDefaultSpeechDevicePermissionWorkflowProfile( + !SessionConfig.DevicePermissionWorkflowProfileId.IsEmpty() + ? SessionConfig.DevicePermissionWorkflowProfileId + : TEXT("speech-device-permission-workflow-v1") + ); + + FHyperTwistSpeechDevicePermissionWorkflowState WorkflowState; + WorkflowState.DevicePermissionWorkflowProfileId = + Profile.DevicePermissionWorkflowProfileId; + for (const FHyperTwistSpeechShellActionBinding& ActionBinding : Profile.ActionBindings) + { + WorkflowState.AvailableActionIds.AddUnique(ActionBinding.ActionId); + } + + if (PriorState != nullptr + && PriorState->DevicePermissionWorkflowState.IsStructurallyValid() + && PriorState->DevicePermissionWorkflowState.DevicePermissionWorkflowProfileId + == Profile.DevicePermissionWorkflowProfileId) + { + WorkflowState = PriorState->DevicePermissionWorkflowState; + WorkflowState.DevicePermissionWorkflowProfileId = + Profile.DevicePermissionWorkflowProfileId; + WorkflowState.AvailableActionIds.Reset(); + for (const FHyperTwistSpeechShellActionBinding& ActionBinding : Profile.ActionBindings) + { + WorkflowState.AvailableActionIds.AddUnique(ActionBinding.ActionId); + } + } + + return WorkflowState; + } + FHyperTwistSpeechUsageCostDashboardShellState BuildSpeechUsageCostDashboardShellState( const FHyperTwistSpeechSessionConfig& SessionConfig, const FHyperTwistTrainingCompanionSpeechSessionState* PriorState @@ -8379,6 +8696,13 @@ FHyperTwistTrainingCompanionSpeechSessionState UHyperTwistTrainingSubsystem::Get return ActiveCompanionSpeechSessionState; } +#if WITH_AUTOMATION_TESTS +void UHyperTwistTrainingSubsystem::RefreshCompanionSpeechServiceHealthForAutomation() +{ + RefreshCompanionSpeechServiceHealth(); +} +#endif + TArray UHyperTwistTrainingSubsystem::GetActiveCoachSignals() const { return ActiveCoachSignals; @@ -12140,6 +12464,11 @@ bool UHyperTwistTrainingSubsystem::OpenActiveCompanionSpeechSession(FString& Out ActiveCompanionSpeechSessionState.FinalTranscriptCount = 0; ActiveCompanionSpeechSessionState.bHasTranscriptResult = false; ActiveCompanionSpeechSessionState.LastTranscriptResult = FHyperTwistSpeechTranscriptResult(); + ActiveCompanionSpeechSessionState.DevicePermissionWorkflowState = + HyperTwistTrainingSubsystemInternal::BuildSpeechDevicePermissionWorkflowState( + SessionConfig, + nullptr + ); ActiveCompanionSpeechSessionState.UsageCostHistoryExportShellState = HyperTwistTrainingSubsystemInternal::BuildSpeechUsageCostHistoryExportShellState( SessionConfig, @@ -12231,6 +12560,9 @@ FHyperTwistSpeechTranscriptResult UHyperTwistTrainingSubsystem::SubmitActiveComp HyperTwistTrainingSubsystemInternal::SynchronizeSpeechMicrophoneShellState( ActiveCompanionSpeechSessionState ); + HyperTwistTrainingSubsystemInternal::SynchronizeSpeechDevicePermissionWorkflowState( + ActiveCompanionSpeechSessionState + ); HyperTwistTrainingSubsystemInternal::SynchronizeSpeechUsageCostDashboardState( ActiveCompanionSpeechSessionState ); @@ -12417,6 +12749,9 @@ FHyperTwistSpeechTranscriptResult UHyperTwistTrainingSubsystem::SubmitActiveComp HyperTwistTrainingSubsystemInternal::SynchronizeSpeechMicrophoneShellState( ActiveCompanionSpeechSessionState ); + HyperTwistTrainingSubsystemInternal::SynchronizeSpeechDevicePermissionWorkflowState( + ActiveCompanionSpeechSessionState + ); HyperTwistTrainingSubsystemInternal::SynchronizeSpeechUsageCostDashboardState( ActiveCompanionSpeechSessionState ); @@ -13015,6 +13350,31 @@ FHyperTwistSpeechSessionConfig UHyperTwistTrainingSubsystem::BuildActiveCompanio SessionConfig.MicrophoneShellProfileDefinition.MicrophoneShellProfileId = SessionConfig.MicrophoneShellProfileId; } + if (SessionConfig.DevicePermissionWorkflowProfileId.IsEmpty() + && SessionConfig.DevicePermissionWorkflowProfileDefinition.IsStructurallyValid()) + { + SessionConfig.DevicePermissionWorkflowProfileId = + SessionConfig.DevicePermissionWorkflowProfileDefinition + .DevicePermissionWorkflowProfileId; + } + if (SessionConfig.DevicePermissionWorkflowProfileId.IsEmpty()) + { + SessionConfig.DevicePermissionWorkflowProfileId = + TEXT("speech-device-permission-workflow-v1"); + } + if (!SessionConfig.DevicePermissionWorkflowProfileDefinition.IsStructurallyValid()) + { + SessionConfig.DevicePermissionWorkflowProfileDefinition = + HyperTwistTrainingSubsystemInternal::BuildDefaultSpeechDevicePermissionWorkflowProfile( + SessionConfig.DevicePermissionWorkflowProfileId + ); + } + if (SessionConfig.DevicePermissionWorkflowProfileDefinition.DevicePermissionWorkflowProfileId + .IsEmpty()) + { + SessionConfig.DevicePermissionWorkflowProfileDefinition.DevicePermissionWorkflowProfileId = + SessionConfig.DevicePermissionWorkflowProfileId; + } SessionConfig.bEnableVad = true; if (!SessionConfig.VadPolicy.IsStructurallyValid()) { @@ -13175,6 +13535,9 @@ void UHyperTwistTrainingSubsystem::RefreshCompanionSpeechServiceHealth() HyperTwistTrainingSubsystemInternal::SynchronizeSpeechMicrophoneShellState( ActiveCompanionSpeechSessionState ); + HyperTwistTrainingSubsystemInternal::SynchronizeSpeechDevicePermissionWorkflowState( + ActiveCompanionSpeechSessionState + ); HyperTwistTrainingSubsystemInternal::SynchronizeSpeechUsageCostDashboardState( ActiveCompanionSpeechSessionState ); @@ -13215,6 +13578,9 @@ void UHyperTwistTrainingSubsystem::RefreshCompanionSpeechServiceHealth() HyperTwistTrainingSubsystemInternal::SynchronizeSpeechMicrophoneShellState( ActiveCompanionSpeechSessionState ); + HyperTwistTrainingSubsystemInternal::SynchronizeSpeechDevicePermissionWorkflowState( + ActiveCompanionSpeechSessionState + ); HyperTwistTrainingSubsystemInternal::SynchronizeSpeechUsageCostDashboardState( ActiveCompanionSpeechSessionState ); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistRecognition/HyperTwistRecognitionTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistRecognition/HyperTwistRecognitionTypes.h index 72b4daa..2dc9518 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistRecognition/HyperTwistRecognitionTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistRecognition/HyperTwistRecognitionTypes.h @@ -1806,6 +1806,297 @@ struct FHyperTwistSpeechMicrophoneShellState } }; +USTRUCT(BlueprintType) +struct FHyperTwistSpeechDevicePermissionWorkflowProfile +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DevicePermissionWorkflowProfileId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString WorkflowKind = TEXT("operator-device-permission-workflow"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PermissionContractId = TEXT("device-permission-workflow-v1"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SettingsHandoffContractId = TEXT("device-permission-settings-handoff-v1"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PermissionRecheckContractId = TEXT("device-permission-recheck-v1"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PermissionRequestActionId = TEXT("request-microphone-permission"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString OpenSettingsActionId = TEXT("open-microphone-privacy-settings"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PermissionRecheckActionId = TEXT("recheck-microphone-permission"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bSupportsPermissionRequest = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bSupportsSettingsHandoff = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bSupportsPermissionRecheck = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Panels; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray ActionBindings; + + bool IsStructurallyValid() const + { + if (DevicePermissionWorkflowProfileId.IsEmpty() + || WorkflowKind.IsEmpty() + || PermissionContractId.IsEmpty() + || SettingsHandoffContractId.IsEmpty() + || PermissionRecheckContractId.IsEmpty() + || PermissionRequestActionId.IsEmpty() + || OpenSettingsActionId.IsEmpty() + || PermissionRecheckActionId.IsEmpty() + || Panels.Num() <= 0 + || ActionBindings.Num() <= 0) + { + return false; + } + + for (const FHyperTwistSpeechShellPanelLayout& Panel : Panels) + { + if (!Panel.IsStructurallyValid()) + { + return false; + } + } + + for (const FHyperTwistSpeechShellActionBinding& ActionBinding : ActionBindings) + { + if (!ActionBinding.IsStructurallyValid()) + { + return false; + } + } + + return true; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistSpeechDevicePermissionWorkflowEntry +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceKind; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString WorkflowStateId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DetailLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RecommendedActionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRequiresSettingsHandoff = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRequiresPermissionRecheck = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bPermissionGranted = true; + + bool IsStructurallyValid() const + { + return !EntryId.IsEmpty() + && !SourceKind.IsEmpty() + && !WorkflowStateId.IsEmpty() + && !StatusLine.IsEmpty() + && !DetailLine.IsEmpty() + && !RecommendedActionId.IsEmpty(); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistSpeechDevicePermissionWorkflowIssue +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString IssueId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString IssueKind; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DetailLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RecommendedActionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bBlocksCaptureStart = true; + + bool IsStructurallyValid() const + { + return !IssueId.IsEmpty() + && !IssueKind.IsEmpty() + && !StatusLine.IsEmpty() + && !DetailLine.IsEmpty() + && !RecommendedActionId.IsEmpty(); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistSpeechDevicePermissionWorkflowState +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DevicePermissionWorkflowProfileId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString WorkflowStateId = TEXT("permission-workflow-cleared"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PermissionStateId = TEXT("permission-granted"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LatestSourceKind = TEXT("permission-state-granted"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StatusLine = TEXT("Microphone permission already granted."); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DetailLine = + TEXT("The first-party permission workflow is clear for the bounded speech shell."); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LastPermissionError; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 WorkflowEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 PermissionRequestEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SettingsHandoffEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 PermissionRecheckEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bPermissionGranted = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bWorkflowSummaryReady = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bPermissionWorkflowReady = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bSettingsReviewRecommended = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bPermissionRequestVisible = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bSettingsHandoffVisible = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bPermissionRecheckVisible = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ActiveIssueCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistSpeechDevicePermissionWorkflowIssue ActiveIssue; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Entries; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Issues; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray AvailableActionIds; + + bool IsStructurallyValid() const + { + if (DevicePermissionWorkflowProfileId.IsEmpty() + || WorkflowStateId.IsEmpty() + || PermissionStateId.IsEmpty() + || LatestSourceKind.IsEmpty() + || StatusLine.IsEmpty() + || DetailLine.IsEmpty() + || WorkflowEntryCount < 0 + || PermissionRequestEntryCount < 0 + || SettingsHandoffEntryCount < 0 + || PermissionRecheckEntryCount < 0 + || ActiveIssueCount < 0 + || AvailableActionIds.Num() <= 0) + { + return false; + } + + if (WorkflowEntryCount != Entries.Num() + || ActiveIssueCount != Issues.Num() + || PermissionRequestEntryCount > WorkflowEntryCount + || SettingsHandoffEntryCount > WorkflowEntryCount + || PermissionRecheckEntryCount > WorkflowEntryCount) + { + return false; + } + + if (ActiveIssueCount > 0 && !ActiveIssue.IsStructurallyValid()) + { + return false; + } + + for (const FHyperTwistSpeechDevicePermissionWorkflowEntry& Entry : Entries) + { + if (!Entry.IsStructurallyValid()) + { + return false; + } + } + + for (const FHyperTwistSpeechDevicePermissionWorkflowIssue& Issue : Issues) + { + if (!Issue.IsStructurallyValid()) + { + return false; + } + } + + for (const FString& ActionId : AvailableActionIds) + { + if (ActionId.IsEmpty()) + { + return false; + } + } + + return true; + } +}; + USTRUCT(BlueprintType) struct FHyperTwistSpeechModelPayloadDescriptor { @@ -4471,6 +4762,14 @@ struct FHyperTwistSpeechSessionConfig UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FHyperTwistSpeechMicrophoneShellProfile MicrophoneShellProfileDefinition; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DevicePermissionWorkflowProfileId = + TEXT("speech-device-permission-workflow-v1"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistSpeechDevicePermissionWorkflowProfile + DevicePermissionWorkflowProfileDefinition; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FString ProviderProfileId = TEXT("speech-provider/local-http-sidecar-profile-v1"); @@ -4577,6 +4876,8 @@ struct FHyperTwistSpeechSessionConfig || TaskKind.IsEmpty() || MicrophoneShellProfileId.IsEmpty() || !MicrophoneShellProfileDefinition.IsStructurallyValid() + || DevicePermissionWorkflowProfileId.IsEmpty() + || !DevicePermissionWorkflowProfileDefinition.IsStructurallyValid() || ProviderProfileId.IsEmpty() || !ProviderProfileDefinition.IsStructurallyValid() || ByokCustodyProfileId.IsEmpty() diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h index 5cfad6c..9ec7bee 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -179,6 +179,10 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Speech") FHyperTwistTrainingCompanionSpeechSessionState GetActiveCompanionSpeechSessionState() const; +#if WITH_AUTOMATION_TESTS + void RefreshCompanionSpeechServiceHealthForAutomation(); +#endif + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") TArray GetActiveCoachSignals() const; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 2dfaa20..e0ab373 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -1940,6 +1940,9 @@ struct FHyperTwistTrainingCompanionSpeechSessionState UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FHyperTwistSpeechMicrophoneShellState MicrophoneShellState; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FHyperTwistSpeechDevicePermissionWorkflowState DevicePermissionWorkflowState; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FHyperTwistSpeechUsageCostDashboardShellState UsageCostDashboardShellState; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistWhisperCppPhase6RAFDevicePermissionWorkflowContractTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistWhisperCppPhase6RAFDevicePermissionWorkflowContractTest.cpp new file mode 100644 index 0000000..816c448 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistWhisperCppPhase6RAFDevicePermissionWorkflowContractTest.cpp @@ -0,0 +1,267 @@ +// Copyright HyperTwist, Inc. All Rights Reserved. + +#include "Misc/AutomationTest.h" + +#include "Engine/GameInstance.h" +#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h" +#include "UObject/UnrealType.h" + +#if WITH_AUTOMATION_TESTS + +namespace HyperTwistWhisperCppPhase6RAFTestInternal +{ + FHyperTwistTrainingDeck MakeSpeechDeck() + { + FHyperTwistTrainingDeck Deck; + Deck.DeckId = TEXT("phase6r-af/whispercpp-device-permission-workflow"); + Deck.Title = TEXT("Phase 6R-AF Whisper Device Permission Workflow"); + Deck.DeliveryModes = { + EHyperTwistTrainingDeliveryMode::CoachReviewed + }; + + FHyperTwistTrainingCase TrainingCase; + TrainingCase.CaseId = TEXT("phase6r-af-case"); + TrainingCase.PuzzleId = TEXT("cube/3x3x3"); + TrainingCase.PromptKind = EHyperTwistTrainingPromptKind::Sequence; + TrainingCase.PromptLabel = TEXT("Phase 6R-AF Coach Speech"); + TrainingCase.AllowedDeliveryModes = { + EHyperTwistTrainingDeliveryMode::CoachReviewed + }; + Deck.Cases = {TrainingCase}; + return Deck; + } + + void ForceSpeechClientKind(UHyperTwistTrainingSubsystem* TrainingSubsystem, const FString& ClientKind) + { + if (TrainingSubsystem == nullptr) + { + return; + } + + if (FStrProperty* SpeechClientKindProperty = FindFProperty( + UHyperTwistTrainingSubsystem::StaticClass(), + TEXT("CompanionSpeechClientKind") + )) + { + SpeechClientKindProperty->SetPropertyValue_InContainer(TrainingSubsystem, ClientKind); + } + } + + UHyperTwistTrainingSubsystem* MakeSpeechSubsystem(const FString& SessionId, const FString& ClientKind) + { + UGameInstance* GameInstance = NewObject(GetTransientPackage()); + if (GameInstance == nullptr) + { + return nullptr; + } + + UHyperTwistTrainingSubsystem* TrainingSubsystem = NewObject(GameInstance); + if (TrainingSubsystem == nullptr) + { + return nullptr; + } + + ForceSpeechClientKind(TrainingSubsystem, ClientKind); + const FHyperTwistTrainingRunState RunState = TrainingSubsystem->StartTrainingRunFromDeck( + MakeSpeechDeck(), + TEXT("phase6r-af-user"), + SessionId, + EHyperTwistTrainingDeliveryMode::CoachReviewed + ); + return RunState.IsStructurallyValid() ? TrainingSubsystem : nullptr; + } + + FHyperTwistTrainingCompanionSpeechSessionState* ResolveActiveSpeechSessionState( + UHyperTwistTrainingSubsystem* TrainingSubsystem + ) + { + if (TrainingSubsystem == nullptr) + { + return nullptr; + } + + if (FStructProperty* SessionStateProperty = FindFProperty( + UHyperTwistTrainingSubsystem::StaticClass(), + TEXT("ActiveCompanionSpeechSessionState") + )) + { + return SessionStateProperty->ContainerPtrToValuePtr( + TrainingSubsystem + ); + } + + return nullptr; + } + + void ForcePermissionError(UHyperTwistTrainingSubsystem* TrainingSubsystem, const FString& ErrorText) + { + if (FHyperTwistTrainingCompanionSpeechSessionState* SessionState = + ResolveActiveSpeechSessionState(TrainingSubsystem)) + { + SessionState->LastError = ErrorText; + SessionState->ServiceHealth.LastError = ErrorText; + } + } +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistWhisperCppPhase6RAFDevicePermissionWorkflowSessionConfigTest, + "HyperTwist.Permissive.WhisperCpp.Phase6R.AF.DevicePermissionWorkflowSessionConfig", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistWhisperCppPhase6RAFDevicePermissionWorkflowSessionConfigTest::RunTest( + const FString& Parameters +) +{ + UHyperTwistTrainingSubsystem* TrainingSubsystem = + HyperTwistWhisperCppPhase6RAFTestInternal::MakeSpeechSubsystem( + TEXT("phase6r-af-config-session"), + TEXT("mock") + ); + TestNotNull(TEXT("The speech subsystem must be constructed for Phase 6R-AF."), TrainingSubsystem); + if (TrainingSubsystem == nullptr) + { + return false; + } + + FString OpenError; + TestTrue(TEXT("The device-permission workflow route must open a speech session."), TrainingSubsystem->OpenActiveCompanionSpeechSession(OpenError)); + TestTrue(TEXT("Opening the speech session must not report an error."), OpenError.IsEmpty()); + + const FHyperTwistTrainingCompanionSpeechSessionState SessionState = + TrainingSubsystem->GetActiveCompanionSpeechSessionState(); + const FHyperTwistSpeechDevicePermissionWorkflowProfile& Profile = + SessionState.SessionConfig.DevicePermissionWorkflowProfileDefinition; + + TestTrue(TEXT("The device-permission workflow route must expose a structurally valid profile."), Profile.IsStructurallyValid()); + TestEqual(TEXT("The workflow profile must preserve the first-party profile id."), Profile.DevicePermissionWorkflowProfileId, TEXT("speech-device-permission-workflow-v1")); + TestEqual(TEXT("The workflow profile must preserve the workflow kind."), Profile.WorkflowKind, TEXT("operator-device-permission-workflow")); + TestEqual(TEXT("The workflow profile must preserve the permission contract id."), Profile.PermissionContractId, TEXT("device-permission-workflow-v1")); + TestEqual(TEXT("The workflow profile must preserve the settings handoff contract id."), Profile.SettingsHandoffContractId, TEXT("device-permission-settings-handoff-v1")); + TestEqual(TEXT("The workflow profile must preserve the recheck contract id."), Profile.PermissionRecheckContractId, TEXT("device-permission-recheck-v1")); + TestEqual(TEXT("The workflow profile must preserve the request action id."), Profile.PermissionRequestActionId, TEXT("request-microphone-permission")); + TestEqual(TEXT("The workflow profile must preserve the settings handoff action id."), Profile.OpenSettingsActionId, TEXT("open-microphone-privacy-settings")); + TestEqual(TEXT("The workflow profile must preserve the recheck action id."), Profile.PermissionRecheckActionId, TEXT("recheck-microphone-permission")); + TestTrue(TEXT("The speech service health must expose device-permission workflow capability."), SessionState.ServiceHealth.Capabilities.Contains(TEXT("devicePermissionWorkflow"))); + TestTrue(TEXT("The speech service health must expose settings handoff capability."), SessionState.ServiceHealth.Capabilities.Contains(TEXT("devicePermissionSettingsHandoff"))); + TestTrue(TEXT("The speech service health must expose permission recheck capability."), SessionState.ServiceHealth.Capabilities.Contains(TEXT("devicePermissionRecheck"))); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistWhisperCppPhase6RAFDevicePermissionWorkflowTransitionTest, + "HyperTwist.Permissive.WhisperCpp.Phase6R.AF.DevicePermissionWorkflowTransitions", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistWhisperCppPhase6RAFDevicePermissionWorkflowTransitionTest::RunTest( + const FString& Parameters +) +{ + UHyperTwistTrainingSubsystem* TrainingSubsystem = + HyperTwistWhisperCppPhase6RAFTestInternal::MakeSpeechSubsystem( + TEXT("phase6r-af-transition-session"), + TEXT("mock") + ); + TestNotNull(TEXT("The speech subsystem must be constructed for Phase 6R-AF."), TrainingSubsystem); + if (TrainingSubsystem == nullptr) + { + return false; + } + + FString OpenError; + TestTrue(TEXT("The device-permission workflow route must open a speech session."), TrainingSubsystem->OpenActiveCompanionSpeechSession(OpenError)); + TestTrue(TEXT("Opening the speech session must not report an error."), OpenError.IsEmpty()); + + FHyperTwistTrainingCompanionSpeechSessionState SessionState = + TrainingSubsystem->GetActiveCompanionSpeechSessionState(); + FHyperTwistSpeechDevicePermissionWorkflowState WorkflowState = + SessionState.DevicePermissionWorkflowState; + + TestTrue(TEXT("The granted workflow state must be structurally valid."), WorkflowState.IsStructurallyValid()); + TestEqual(TEXT("The granted workflow state must start cleared."), WorkflowState.WorkflowStateId, TEXT("permission-workflow-cleared")); + TestEqual(TEXT("The granted workflow state must preserve permission-granted posture."), WorkflowState.PermissionStateId, TEXT("permission-granted")); + TestTrue(TEXT("The granted workflow state must report permission granted."), WorkflowState.bPermissionGranted); + TestTrue(TEXT("The granted workflow state must report workflow ready."), WorkflowState.bPermissionWorkflowReady); + TestFalse(TEXT("The granted workflow state must keep permission request hidden."), WorkflowState.bPermissionRequestVisible); + + HyperTwistWhisperCppPhase6RAFTestInternal::ForcePermissionError( + TrainingSubsystem, + TEXT("microphone-permission-required") + ); + TrainingSubsystem->RefreshCompanionSpeechServiceHealthForAutomation(); + + SessionState = TrainingSubsystem->GetActiveCompanionSpeechSessionState(); + WorkflowState = SessionState.DevicePermissionWorkflowState; + + TestTrue(TEXT("The request-required workflow state must remain structurally valid."), WorkflowState.IsStructurallyValid()); + TestEqual(TEXT("The request-required workflow state must preserve the request posture."), WorkflowState.WorkflowStateId, TEXT("permission-request-required")); + TestEqual(TEXT("The request-required workflow state must preserve the permission-required posture."), WorkflowState.PermissionStateId, TEXT("permission-required")); + TestFalse(TEXT("The request-required workflow state must report permission withheld."), WorkflowState.bPermissionGranted); + TestFalse(TEXT("The request-required workflow state must block workflow ready."), WorkflowState.bPermissionWorkflowReady); + TestTrue(TEXT("The request-required workflow state must surface the permission request action."), WorkflowState.bPermissionRequestVisible); + TestFalse(TEXT("The request-required workflow state must keep settings handoff hidden."), WorkflowState.bSettingsHandoffVisible); + TestFalse(TEXT("The request-required workflow state must keep permission recheck hidden."), WorkflowState.bPermissionRecheckVisible); + TestEqual(TEXT("The request-required workflow state must retain one workflow entry."), WorkflowState.WorkflowEntryCount, 1); + TestEqual(TEXT("The request-required workflow state must retain one request entry."), WorkflowState.PermissionRequestEntryCount, 1); + TestEqual(TEXT("The request-required workflow state must classify the active issue correctly."), WorkflowState.ActiveIssue.IssueKind, TEXT("permission-request-required")); + return true; +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FHyperTwistWhisperCppPhase6RAFDevicePermissionSettingsWorkflowTest, + "HyperTwist.Permissive.WhisperCpp.Phase6R.AF.DevicePermissionSettingsWorkflow", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter +) + +bool FHyperTwistWhisperCppPhase6RAFDevicePermissionSettingsWorkflowTest::RunTest( + const FString& Parameters +) +{ + UHyperTwistTrainingSubsystem* TrainingSubsystem = + HyperTwistWhisperCppPhase6RAFTestInternal::MakeSpeechSubsystem( + TEXT("phase6r-af-settings-session"), + TEXT("mock") + ); + TestNotNull(TEXT("The speech subsystem must be constructed for Phase 6R-AF."), TrainingSubsystem); + if (TrainingSubsystem == nullptr) + { + return false; + } + + FString OpenError; + TestTrue(TEXT("The settings-review workflow route must open a speech session."), TrainingSubsystem->OpenActiveCompanionSpeechSession(OpenError)); + TestTrue(TEXT("Opening the settings-review speech session must not report an error."), OpenError.IsEmpty()); + + HyperTwistWhisperCppPhase6RAFTestInternal::ForcePermissionError( + TrainingSubsystem, + TEXT("microphone-permission-denied-settings") + ); + TrainingSubsystem->RefreshCompanionSpeechServiceHealthForAutomation(); + + const FHyperTwistTrainingCompanionSpeechSessionState SessionState = + TrainingSubsystem->GetActiveCompanionSpeechSessionState(); + const FHyperTwistSpeechDevicePermissionWorkflowState& WorkflowState = + SessionState.DevicePermissionWorkflowState; + + TestTrue(TEXT("The settings-review workflow state must remain structurally valid."), WorkflowState.IsStructurallyValid()); + TestEqual(TEXT("The settings-review workflow must preserve the settings-review posture."), WorkflowState.WorkflowStateId, TEXT("permission-settings-review-required")); + TestEqual(TEXT("The settings-review workflow must preserve permission-required posture."), WorkflowState.PermissionStateId, TEXT("permission-required")); + TestEqual(TEXT("The settings-review workflow must preserve the settings-review source kind."), WorkflowState.LatestSourceKind, TEXT("permission-settings-review-required")); + TestFalse(TEXT("The settings-review workflow must report permission withheld."), WorkflowState.bPermissionGranted); + TestFalse(TEXT("The settings-review workflow must block workflow-ready posture."), WorkflowState.bPermissionWorkflowReady); + TestTrue(TEXT("The settings-review workflow must recommend settings review."), WorkflowState.bSettingsReviewRecommended); + TestTrue(TEXT("The settings-review workflow must surface settings handoff."), WorkflowState.bSettingsHandoffVisible); + TestTrue(TEXT("The settings-review workflow must surface permission recheck."), WorkflowState.bPermissionRecheckVisible); + TestEqual(TEXT("The settings-review workflow must retain one workflow entry."), WorkflowState.WorkflowEntryCount, 1); + TestEqual(TEXT("The settings-review workflow must retain one settings handoff entry."), WorkflowState.SettingsHandoffEntryCount, 1); + TestEqual(TEXT("The settings-review workflow must retain one permission recheck entry."), WorkflowState.PermissionRecheckEntryCount, 1); + TestEqual(TEXT("The settings-review workflow must classify the active issue correctly."), WorkflowState.ActiveIssue.IssueKind, TEXT("permission-settings-review-required")); + TestTrue(TEXT("The health surface must still advertise device-permission workflow capability."), SessionState.ServiceHealth.Capabilities.Contains(TEXT("devicePermissionWorkflow"))); + TestTrue(TEXT("The health surface must still advertise settings handoff capability."), SessionState.ServiceHealth.Capabilities.Contains(TEXT("devicePermissionSettingsHandoff"))); + TestTrue(TEXT("The health surface must still advertise permission recheck capability."), SessionState.ServiceHealth.Capabilities.Contains(TEXT("devicePermissionRecheck"))); + return true; +} + +#endif diff --git a/docs/REPO_LICENSE_TRACKING.md b/docs/REPO_LICENSE_TRACKING.md index d59c57f..8ce0716 100644 --- a/docs/REPO_LICENSE_TRACKING.md +++ b/docs/REPO_LICENSE_TRACKING.md @@ -211,8 +211,13 @@ Status update on `2026-05-21`: external-portal handoff control pass is now consumed - the bounded first-party `Phase 6R-AE` provider settlement exception and external-portal handoff packet is now landed in current code -- the current next bounded move is a source-backed `Phase 6R-AF` first-party real - device-permission workflow preparation/control pass, not a new restrictive packet by default +- the generic source-backed `Phase 6R-AF` first-party real device-permission workflow control + pass is now consumed +- the bounded first-party `Phase 6R-AF` real device-permission workflow packet is now landed in + current code +- the current next bounded move is a source-backed `Phase 6R-AG` first-party native + capture-route ownership and workflow preparation/control pass, not a new restrictive packet by + default - use the repo-row README census, the portfolio standing refresh backfill, and the `2R-A` ownership contract for the current queue after that correction @@ -361,8 +366,8 @@ Current practical interpretation: - the landed `PostHog/posthog` boundary-sensitive widening packet now lives in `docs/HYPERTWIST_PHASE_4R_PACKET_4R_D_POSTHOG_CONTROL_PLANE_IMPLEMENTATION_2026-05-13.md` - the landed `screenpipe/screenpipe` boundary-sensitive widening packet now lives in `docs/HYPERTWIST_PHASE_4R_PACKET_4R_E_SCREENPIPE_CAPTURE_HISTORY_IMPLEMENTATION_2026-05-13.md` - the landed `remotion-dev/remotion` boundary-sensitive widening packet now lives in `docs/HYPERTWIST_PHASE_4R_PACKET_4R_F_REMOTION_MEDIA_EXPORT_IMPLEMENTATION_2026-05-13.md` -- the next bounded move is a source-backed `Phase 6R-AE` first-party provider settlement - exception and external-portal handoff preparation/control pass +- the next bounded move is a source-backed `Phase 6R-AG` first-party native capture-route + ownership and workflow preparation/control pass Companion docs: @@ -1618,6 +1623,8 @@ Approved working posture: profile and shell-state boundary above the existing transcript-session seam - the next bounded permissive implementation slice is now landed as device-permission and capture-route readiness shell posture above the existing microphone shell seam +- the next bounded first-party implementation slice is now landed as real device-permission + workflow posture above the existing permission/readiness shell seam - the next bounded permissive implementation slice is now landed as downloadable model and payload custody metadata above the existing transcript-session and shell seams - the next bounded first-party implementation slice is now landed as provider-profile and BYOK @@ -1636,11 +1643,14 @@ Approved working posture: exception and external-portal handoff shell metadata above the existing transcript-session, shell, payload-custody, provider-custody, provider-routing, usage/cost, history/export, receipt-review, and settlement seams +- the next bounded first-party implementation slice is now landed as real device-permission + workflow metadata above the existing transcript-session, shell, permission/readiness, + payload-custody, provider-custody, provider-routing, usage/cost, history/export, + receipt-review, settlement, and exception/handoff seams - the top-level provider/session contract remains first-party HyperTwist-owned and provider-neutral; this row does not own that lane -- keep real device-permission workflow, native capture-route ownership, actual payment - execution, provider-portal ownership, actual payload shipping, and future voice-asset review - separate from the code-license judgment +- keep native capture-route ownership, actual payment execution, provider-portal ownership, + actual payload shipping, and future voice-asset review separate from the code-license judgment - treat `SYSTRAN/faster-whisper` as the landed complementary Python-orchestration donor rather than widening `whisper.cpp` into the runtime core diff --git a/docs/arch/HYPERTWIST_PHASE6R_AF_FIRST_PARTY_REAL_DEVICE_PERMISSION_WORKFLOW_IMPLEMENTATION_PACKET_2026-05-25.md b/docs/arch/HYPERTWIST_PHASE6R_AF_FIRST_PARTY_REAL_DEVICE_PERMISSION_WORKFLOW_IMPLEMENTATION_PACKET_2026-05-25.md new file mode 100644 index 0000000..62408f6 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE6R_AF_FIRST_PARTY_REAL_DEVICE_PERMISSION_WORKFLOW_IMPLEMENTATION_PACKET_2026-05-25.md @@ -0,0 +1,168 @@ +# HyperTwist Phase 6R-AF first-party real device-permission workflow implementation packet + +Created on `2026-05-25` + +## Status + +- first-party HyperTwist packet +- bounded `Phase 6R-AF` implementation slice + +## Purpose + +This packet lands the next bounded first-party slice above the landed speech +session, microphone-shell, permission/readiness shell, payload-custody, +provider-custody, provider-routing, normalized usage/cost event, +operator-facing usage/cost dashboard, provider usage/cost history/export, +provider receipt-review, provider billing-settlement, and provider settlement +exception seams. + +The landed slice is: + +- first-party real device-permission workflow + +It is not: + +- a native audio-device route ownership packet +- an actual OS permission-grant execution packet +- an actual payment execution packet +- a provider-portal ownership packet +- an actual payload-shipping packet +- a broad assistant-platform packet + +## Current authority basis + +This implementation packet stands on: + +- `docs/ops/HYPERTWIST_PROVIDER_NEUTRALITY_AND_BYOK_DOCTRINE_2026-05-21.md` +- `docs/arch/HYPERTWIST_PHASE6R_AF_FIRST_PARTY_REAL_DEVICE_PERMISSION_WORKFLOW_PREPARATION_PACKET_2026-05-25.md` +- `docs/arch/HYPERTWIST_PHASE6R_V_WHISPER_CPP_DEVICE_PERMISSION_AND_CAPTURE_ROUTE_READINESS_SHELL_IMPLEMENTATION_PACKET_2026-05-24.md` + +The preserved owners do not change: + +- first-party HyperTwist remains the top-level provider/session owner +- first-party HyperTwist remains the usage/cost governance owner +- first-party HyperTwist remains the real device-permission workflow owner +- `ggml-org/whisper.cpp` remains bounded to offline STT donor seams +- `SYSTRAN/faster-whisper` remains bounded to complementary Python + orchestration/service-lane seams + +## Landed scope + +The current code now owns a bounded permission-workflow seam through: + +- retained recognition contract types for: + - `FHyperTwistSpeechDevicePermissionWorkflowProfile` + - `FHyperTwistSpeechDevicePermissionWorkflowEntry` + - `FHyperTwistSpeechDevicePermissionWorkflowIssue` + - `FHyperTwistSpeechDevicePermissionWorkflowState` + - expanded `FHyperTwistSpeechSessionConfig` + - expanded `FHyperTwistTrainingCompanionSpeechSessionState` +- contract-library defaults for: + - permission-workflow profile ids and definitions + - permission panel ids and action bindings + - service-health capability exposure +- active companion session-config, service-health, and derived + permission-workflow composition in: + - `UHyperTwistTrainingSubsystem` +- direct speech-client health capability exposure in: + - `UHyperTwistHttpSpeechClient` + - `UHyperTwistMockSpeechClient` +- focused automation coverage in: + - `HyperTwistWhisperCppPhase6RAFDevicePermissionWorkflowContractTest.cpp` + +## Why this is still intentionally bounded + +This packet lands the real device-permission workflow family only. + +Still deferred: + +- native audio-device route ownership beyond bounded readiness/workflow posture +- actual OS permission-grant execution ownership +- actual payment execution or provider-portal ownership +- actual downloadable model/payload shipping +- broad assistant-platform scope + +## Validation + +Build validation: + +- `UnrealHyperTwistEditor Win64 Development` + +Focused automation validation: + +- `HyperTwist.Permissive.WhisperCpp.Phase6R.AF` `3/3` + +Regression automation validation: + +- `HyperTwist.Permissive.WhisperCpp.Phase6R.AE` `3/3` +- `HyperTwist.Permissive.WhisperCpp.Phase6R.V` `3/3` +- `HyperTwist.Permissive.WhisperCpp.Phase6R.U` `3/3` +- `HyperTwist.Permissive.WhisperCpp.Phase6R.E` `3/3` +- `HyperTwist.Permissive.FasterWhisper.Phase6R.F` `3/3` +- `HyperTwist.Permissive.Piper.Phase6R.G` `3/3` +- `HyperTwist.Permissive.Coqui.Phase6R.H` `3/3` +- `HyperTwist.CleanRoom.CubeDesk` `14/14` + +Non-blocking warnings stayed limited to the existing Unreal headless/editor +noise, the pre-existing plugin dependency warning on `UnrealMCP`, and the +known `http:/vision/health` hostname-resolution warnings in provider-backed +automation. + +## Queue effect + +This packet consumes the current `Phase 6R-AF` implementation slice. + +`ggml-org/whisper.cpp` remains only partially incorporated: + +- landed now: + - speech transcript session boundary + - live microphone shell boundary + - device-permission and capture-route readiness shell posture + - first-party real device-permission workflow boundary for: + - permission-workflow profile ids and definitions + - request / settings handoff / recheck issue posture + - bounded capability exposure for: + - `devicePermissionWorkflow` + - `devicePermissionSettingsHandoff` + - `devicePermissionRecheck` + - downloadable model and payload custody boundary + - first-party provider-profile and BYOK custody boundary + - first-party provider routing and workflow-policy boundary + - first-party normalized usage/cost event-model boundary + - first-party operator-facing provider usage/cost dashboard shell boundary + - first-party provider usage/cost history/export shell boundary + - first-party provider receipt review and posted-charge inspection shell + boundary + - first-party provider billing settlement and invoice reconciliation shell + boundary + - first-party provider settlement exception and external-portal handoff shell + boundary +- still deferred: + - native audio-device route ownership beyond bounded readiness/workflow + posture + - actual OS permission-grant execution ownership + - actual payment execution or provider-portal ownership + - actual downloadable model/payload shipping + - broad assistant-platform scope + +The next clean move is: + +- a source-backed `Phase 6R-AG` first-party native capture-route ownership and + workflow preparation/control pass + +Keep the future sequencing guards visible: + +- first-party HyperTwist + - keep the top-level provider/session contract, routing/policy lane, + usage/cost governance lane, dashboard shell lane, history/export shell + lane, receipt-review shell lane, settlement shell lane, + exception/handoff shell lane, and permission-workflow lane first-party +- `ggml-org/whisper.cpp` + - keep code-license judgments separate from actual model or payload shipping + review in any follow-on packet +- `SYSTRAN/faster-whisper` + - keep the row complementary to the landed provider/session, custody, + routing, usage/cost, dashboard, history/export, receipt-review, + settlement, exception/handoff, and permission-workflow seams rather than + widening it into native route ownership, payment execution, or + provider-portal ownership diff --git a/docs/arch/HYPERTWIST_PHASE6R_AF_FIRST_PARTY_REAL_DEVICE_PERMISSION_WORKFLOW_PREPARATION_PACKET_2026-05-25.md b/docs/arch/HYPERTWIST_PHASE6R_AF_FIRST_PARTY_REAL_DEVICE_PERMISSION_WORKFLOW_PREPARATION_PACKET_2026-05-25.md new file mode 100644 index 0000000..c224ffd --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE6R_AF_FIRST_PARTY_REAL_DEVICE_PERMISSION_WORKFLOW_PREPARATION_PACKET_2026-05-25.md @@ -0,0 +1,129 @@ +# HyperTwist Phase 6R-AF first-party real device-permission workflow preparation packet + +Created on `2026-05-25` + +## Status + +- first-party HyperTwist packet +- source-backed `Phase 6R-AF` preparation/control slice + +## Purpose + +This packet scopes the next bounded first-party slice above the landed speech +session, microphone-shell, permission/readiness shell, payload-custody, +provider-custody, provider-routing, normalized usage/cost event, +operator-facing usage/cost dashboard, provider usage/cost history/export, +provider receipt-review, provider billing-settlement, and provider settlement +exception seams. + +The granted slice is: + +- first-party real device-permission workflow only + +It is not: + +- a native audio-device route ownership packet +- an actual OS permission-grant execution packet +- an actual payment execution packet +- a provider-portal ownership packet +- an actual payload-shipping packet +- a broad assistant-platform packet + +## Current authority basis + +This control pass stands on: + +- `docs/ops/HYPERTWIST_PROVIDER_NEUTRALITY_AND_BYOK_DOCTRINE_2026-05-21.md` +- `docs/arch/HYPERTWIST_PHASE6R_V_WHISPER_CPP_DEVICE_PERMISSION_AND_CAPTURE_ROUTE_READINESS_SHELL_IMPLEMENTATION_PACKET_2026-05-24.md` +- `docs/arch/HYPERTWIST_PHASE6R_AE_FIRST_PARTY_PROVIDER_SETTLEMENT_EXCEPTION_AND_EXTERNAL_PORTAL_HANDOFF_IMPLEMENTATION_PACKET_2026-05-25.md` +- `docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md` + +The preserved owners do not change here: + +- first-party HyperTwist remains the top-level provider/session owner +- first-party HyperTwist remains the usage/cost governance owner +- first-party HyperTwist remains the real device-permission workflow owner +- `ggml-org/whisper.cpp` remains bounded to offline STT donor seams +- `SYSTRAN/faster-whisper` remains bounded to complementary Python + orchestration/service-lane seams + +## Granted family + +The bounded `Phase 6R-AF` slice may land: + +1. first-party permission-workflow profile ids and definitions +2. derived permission-workflow entry, issue, and shell-state record shapes +3. session-config and companion-session-state expansion for those shapes +4. bounded capability exposure for: + - `devicePermissionWorkflow` + - `devicePermissionSettingsHandoff` + - `devicePermissionRecheck` +5. focused automation for: + - permission-request-required posture + - settings-review-required posture + - permission-recheck posture + +The packet must stay out of: + +- native audio-device route ownership +- actual OS permission-grant execution ownership +- actual payment execution +- provider-portal ownership +- actual payload download or redistribution ownership + +## Proposed implementation shape + +Land the narrower first-party boundary through: + +- retained recognition contract types for: + - permission-workflow profile + - permission-workflow entry + - permission-workflow issue + - permission-workflow state + - expanded speech session config + - expanded companion speech session state +- contract-library defaults for: + - permission-workflow profile ids and definitions + - action bindings and panel ids + - service-health capability exposure +- training-subsystem composition for: + - bounded request / settings handoff / recheck posture + - explicit separation between workflow posture and native route ownership + - explicit separation between workflow posture and actual OS grant execution +- direct speech-client health capability exposure for: + - `devicePermissionWorkflow` + - `devicePermissionSettingsHandoff` + - `devicePermissionRecheck` + +## Validation target + +Validate with: + +- Unreal build for `UnrealHyperTwistEditor Win64 Development` +- focused automation: + - `HyperTwist.Permissive.WhisperCpp.Phase6R.AF` +- regressions: + - `HyperTwist.Permissive.WhisperCpp.Phase6R.AE` + - `HyperTwist.Permissive.WhisperCpp.Phase6R.V` + - `HyperTwist.Permissive.WhisperCpp.Phase6R.U` + - `HyperTwist.Permissive.WhisperCpp.Phase6R.E` + - `HyperTwist.Permissive.FasterWhisper.Phase6R.F` + - `HyperTwist.Permissive.Piper.Phase6R.G` + - `HyperTwist.Permissive.Coqui.Phase6R.H` + - `HyperTwist.CleanRoom.CubeDesk` + +## Queue effect + +If this packet lands cleanly, the next bounded move becomes: + +- a source-backed `Phase 6R-AG` first-party native capture-route ownership and + workflow preparation/control pass + +Keep the narrow sequencing guards visible: + +- keep native capture-route ownership separate from the landed + permission-workflow posture +- keep actual payment execution and provider-portal ownership separate from + this packet +- keep actual payload shipping separate from usage/cost governance +- keep broad assistant-platform widening outside this packet diff --git a/docs/ops/HYPERTWIST_IMPLEMENTATION_PHASE_1_KICKOFF.md b/docs/ops/HYPERTWIST_IMPLEMENTATION_PHASE_1_KICKOFF.md index f1fa101..2567b41 100644 --- a/docs/ops/HYPERTWIST_IMPLEMENTATION_PHASE_1_KICKOFF.md +++ b/docs/ops/HYPERTWIST_IMPLEMENTATION_PHASE_1_KICKOFF.md @@ -188,11 +188,15 @@ The next bounded move is now: external-portal handoff control pass is now consumed 36. the bounded first-party `Phase 6R-AE` provider settlement exception and external-portal handoff packet is now landed in current code -37. the next bounded move is a source-backed `Phase 6R-AF` first-party real - device-permission workflow preparation/control pass -38. keep the speech-lane guard visible: +37. the generic source-backed `Phase 6R-AF` first-party real device-permission workflow + control pass is now consumed +38. the bounded first-party `Phase 6R-AF` real device-permission workflow packet is now + landed in current code +39. the next bounded move is a source-backed `Phase 6R-AG` first-party native + capture-route ownership and workflow preparation/control pass +40. keep the speech-lane guard visible: - keep code-license judgments separate from model, voice, and payload-license review -39. keep the provider-neutral speech-lane guard visible: +41. keep the provider-neutral speech-lane guard visible: - first-party HyperTwist owns the top-level provider/session contract - `whisper.cpp` and `faster-whisper` may win narrower donor slices without inheriting that lane, the provider routing/policy lane, or the usage/cost governance lane diff --git a/docs/ops/HYPERTWIST_PROVIDER_NEUTRALITY_AND_BYOK_DOCTRINE_2026-05-21.md b/docs/ops/HYPERTWIST_PROVIDER_NEUTRALITY_AND_BYOK_DOCTRINE_2026-05-21.md index 7e6d1d9..6963772 100644 --- a/docs/ops/HYPERTWIST_PROVIDER_NEUTRALITY_AND_BYOK_DOCTRINE_2026-05-21.md +++ b/docs/ops/HYPERTWIST_PROVIDER_NEUTRALITY_AND_BYOK_DOCTRINE_2026-05-21.md @@ -224,8 +224,10 @@ Current landed ordering status: bounded first-party implementation anchor - provider settlement exception and external-portal handoff now also have a bounded first-party implementation anchor -- real device-permission workflow is now the next bounded first-party gap - above the landed permission/readiness shell posture +- real device-permission workflow now also has a bounded first-party + implementation anchor above the landed permission/readiness shell posture +- native capture-route ownership and workflow is now the next bounded first-party + gap above the landed readiness and permission-workflow seams - actual payment execution or provider-portal ownership remains later first-party work - provider-specific adapters remain later work diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md index c69c892..e852e8f 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md @@ -131,6 +131,7 @@ Do not collapse those three tiers into one undifferentiated "features" voice. | First-party provider receipt review and posted-charge inspection shell | Implemented now | landed first-party `Phase 6R-AC` | First-party receipt-review shell profiles, derived receipt-review entries, charge-pending and variance issue posture, and bounded inspection capability exposure are now live above the landed provider usage/cost history/export seam. | | First-party provider billing settlement and invoice reconciliation shell | Implemented now | landed first-party `Phase 6R-AD` | First-party settlement shell profiles, derived settlement entries, invoice-reconciliation posture, settlement-readiness issue state, and bounded settlement capability exposure are now live above the landed provider receipt-review seam. | | First-party provider settlement exception and external-portal handoff shell | Implemented now | landed first-party `Phase 6R-AE` | First-party settlement-exception shell profiles, derived exception entries, reference-only external-portal handoff posture, and bounded exception capability exposure are now live above the landed provider settlement seam. | +| First-party real device-permission workflow | Implemented now | landed first-party `Phase 6R-AF` | First-party permission-workflow profiles, request/settings/recheck posture, and bounded workflow capability exposure are now live above the landed permission/readiness shell seam. | | Analytics/reporting surfaces | Implemented now | landed analytics/reporting packets | Reporting is real, but bounded to accepted retained slices. | | Browser/spatial/media adjunct surfaces | Implemented now | landed `three.js`, `react-three-fiber`, `xr`, `model-viewer`, `remotion` packets | These are implemented bounded families, not proof of unlimited browser-shell parity. | @@ -210,6 +211,7 @@ repo. | Speech transcript session boundary | Implemented now | landed `whisper.cpp` packet | Current bounded STT truth. | | Live microphone capture shell | Implemented now | landed `whisper.cpp` `Phase 6R-U` packet | Current bounded microphone shell truth above the landed transcript-session and orchestration seams. | | Device-permission and capture-route readiness shell | Implemented now | landed `whisper.cpp` `Phase 6R-V` packet | Current bounded permission/readiness shell posture, route fallback issue objects, and explicit retry/request semantics above the landed microphone shell seam. | +| First-party real device-permission workflow | Implemented now | landed first-party `Phase 6R-AF` packet | Current bounded permission-workflow profile ids/definitions, request/settings/recheck posture, and workflow capability exposure are live above the landed permission/readiness shell seam. Native capture-route ownership remains deferred. | | Downloadable model and payload custody boundary | Implemented now | landed `whisper.cpp` `Phase 6R-W` packet | Current bounded custody profile, required/supported payload descriptors, and health-surface integrity metadata above the landed transcript-session and shell seams. | | Provider-backed speech session health and transcript envelopes | Implemented now | first-party speech client surfaces | Current first-party speech seam. | | Python transcription-service orchestration profile | Implemented now | landed `faster-whisper` packet | Current bounded Python STT orchestration truth above the existing speech session boundary. | diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md index d215caf..6dd280d 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md @@ -187,10 +187,13 @@ Canonical discovery surfaces for roadmap interpretation: external-portal handoff control pass is now consumed - the bounded first-party `Phase 6R-AE` provider settlement exception and external-portal handoff packet is now landed in current code -- the current next bounded move is a source-backed `Phase 6R-AF` first-party real - device-permission workflow preparation/control pass, while keeping native capture-route - ownership, actual payment execution, provider-portal ownership, and actual payload shipping - separately deferred +- the generic source-backed `Phase 6R-AF` first-party real device-permission workflow control + pass is now consumed +- the bounded first-party `Phase 6R-AF` real device-permission workflow packet is now landed in + current code +- the current next bounded move is a source-backed `Phase 6R-AG` first-party native + capture-route ownership and workflow preparation/control pass, while keeping actual payment + execution, provider-portal ownership, and actual payload shipping separately deferred - the repo-row implementation queue is now live from that `Phase 6R-A` entry point rather than waiting on another first-party packet @@ -277,14 +280,14 @@ Current routing truth: - `brianpeiris/RiftSketch` - the v6.3 CSV label `not_live_reference_or_discard_candidate` is not the final state by itself; read the closed `0R-E` packet for the retained-versus-discarded split -The next bounded move is a source-backed `Phase 6R-AF` first-party real -device-permission workflow preparation/control pass. +The next bounded move is a source-backed `Phase 6R-AG` first-party native +capture-route ownership and workflow preparation/control pass. Queue interpretation after that control pass: - then continue with the retained repo-row queue - highest current repo-row queue pressure sits in: - - the first-party real device-permission workflow remainder above the + - the first-party native capture-route ownership remainder above the partially landed `whisper.cpp` and `faster-whisper` speech seams - `HactarCE/Hyperspeedcube` is now closed for the currently justified retained row: - landed: @@ -455,15 +458,15 @@ Queue interpretation after that control pass: - viseme / gesture runtime integration - broad assistant-platform scope - the next queue shape is now: - - first-party real device-permission workflow assessment above the landed - speech session, shell, payload-custody, provider-custody, provider-routing, - normalized usage/cost, operator-facing dashboard, history/export, - receipt-review, settlement, and exception/handoff seams + - first-party native capture-route ownership and workflow assessment above the + landed speech session, shell, permission/readiness, permission-workflow, + payload-custody, provider-custody, provider-routing, normalized usage/cost, + operator-facing dashboard, history/export, receipt-review, settlement, and + exception/handoff seams - the next bounded move should stay narrow: - - a source-backed `Phase 6R-AF` control pass before any widening into native - capture-route ownership, actual payment execution, provider-portal - ownership, actual payload shipping, broad voice-output ownership, or broad - assistant-platform scope + - a source-backed `Phase 6R-AG` control pass before any widening into actual + payment execution, provider-portal ownership, actual payload shipping, + broad voice-output ownership, or broad assistant-platform scope - keep the speech-input / voice sidecar legal sequencing guard visible: - keep code-license judgments separate from model, voice, and payload-license review - keep the provider-neutral speech-lane guard visible: