Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#if UNITY_EDITOR
using System.Text;
using SecondSpawn.Networking;
using UnityEditor;
using UnityEngine;

namespace SecondSpawn.EditorTools
{
public static class SecondSpawnFacialBlendshapeReportUtility
{
[MenuItem("Second Spawn/Art/Report Selected Facial Blendshapes")]
public static void ReportSelectedFacialBlendshapes()
{
var selected = Selection.activeGameObject;
if (selected == null)
{
Debug.LogWarning("[SecondSpawnFacialBlendshapeReportUtility] Select a character prefab or scene object first.");
return;
}

Debug.Log(BuildReport(selected));
}

[MenuItem("Second Spawn/Art/Report Generated Visual Facial Blendshapes")]
public static void ReportGeneratedVisualFacialBlendshapes()
{
var builder = new StringBuilder();
builder.AppendLine("[SecondSpawnFacialBlendshapeReportUtility] Generated visual facial blendshape report");
for (var variant = 0; variant < VisualPrefabCatalog.Count; variant++)
{
var path = VisualPrefabCatalog.GetCleanAssetPath(variant);
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab == null)
{
path = VisualPrefabCatalog.GetSourceAssetPath(variant);
prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
}

builder.AppendLine($"Variant {variant:00}: {VisualPrefabCatalog.GetLabel(variant)}");
builder.AppendLine(prefab == null
? $" missing prefab at {path}"
: Indent(BuildReport(prefab), " "));
}

Debug.Log(builder.ToString());
}

private static string BuildReport(GameObject root)
{
var builder = new StringBuilder();
builder.AppendLine($"Facial blendshape report for {root.name}");
var renderers = root.GetComponentsInChildren<SkinnedMeshRenderer>(includeInactive: true);
if (renderers.Length == 0)
{
builder.AppendLine("No SkinnedMeshRenderer found.");
return builder.ToString();
}

var anyBlendshapes = false;
foreach (var renderer in renderers)
{
if (renderer == null || renderer.sharedMesh == null || renderer.sharedMesh.blendShapeCount <= 0)
{
continue;
}

anyBlendshapes = true;
builder.AppendLine($"{renderer.name} | mesh={renderer.sharedMesh.name} | blendshapes={renderer.sharedMesh.blendShapeCount}");
for (var index = 0; index < renderer.sharedMesh.blendShapeCount; index++)
{
builder.AppendLine($" {index:00}: {renderer.sharedMesh.GetBlendShapeName(index)}");
}
}

if (!anyBlendshapes)
{
builder.AppendLine("No blendshape-enabled renderer found.");
}

return builder.ToString();
}

private static string Indent(string value, string prefix)
{
if (string.IsNullOrWhiteSpace(value))
{
return "";
}

return prefix + value.TrimEnd().Replace("\n", "\n" + prefix);
}
}
}
#endif

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 74 additions & 0 deletions Unity/Assets/_SecondSpawn/Scripts/AI/AgentContextDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1445,4 +1445,78 @@ public sealed class VoiceSessionDebugDto
public string provider_status;
public string fallback_mode;
}

[Serializable]
public sealed class RealtimeVoiceSessionRequestDto
{
public string actor_id;
public string conversation_session_id;
public string requested_transport = "livekit_ready";
public string input_mode = "text_or_microphone";
public int ttl_seconds = 120;
public bool text_input_supported = true;
public bool microphone_input_supported = true;
public int sample_rate_hz = 16000;
public int channels = 1;
public string client_platform = "unity";
public string provider_hint = "gemini_live_or_tts";
}

[Serializable]
public sealed class RealtimeVoiceSessionDto
{
public bool session_available;
public string provider;
public string reason;
public string actor_id;
public string conversation_session_id;
public VoiceSessionMaterialDto session;
public RealtimeVoiceInputPolicyDto input_policy;
public VoiceSessionDebugDto debug;
}

[Serializable]
public sealed class RealtimeVoiceInputPolicyDto
{
public bool accepts_text;
public bool accepts_audio;
public int max_audio_ms;
public int sample_rate_hz;
public int channels;
public string[] accepted_audio_formats;
}

[Serializable]
public sealed class RealtimeVoiceInputRequestDto
{
public string client_event_id;
public string session_id;
public string actor_id;
public string conversation_session_id;
public string input_kind;
public string text;
public string audio_format;
public int sample_rate_hz;
public int channels;
public int duration_ms;
public string audio_base64;
}

[Serializable]
public sealed class RealtimeVoiceInputResponseDto
{
public bool accepted;
public string provider;
public string reason;
public string conversation_session_id;
public string transcript;
public string npc_actor_id;
public string npc_text;
public string voice_audio_base64;
public string voice_audio_format;
public int voice_sample_rate_hz;
public int voice_channels;
public bool fallback_to_text_chat;
public VoiceSessionDebugDto debug;
}
}
42 changes: 41 additions & 1 deletion Unity/Assets/_SecondSpawn/Scripts/AI/PrototypeAgentBrain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ private enum BrainPhase
private AgentContextDto _context;
private PrototypeSpeechBubble _speechBubble;
private PrototypeVoiceCue _voiceCue;
private PrototypeNpcVoicePresenter _voicePresenter;
private VisualAnimationIntentDriver _intentDriver;
private Animator _animator;
private GameObject _visualRoot;
Expand Down Expand Up @@ -127,6 +128,7 @@ private void Awake()
_baseMoveSpeed = _moveSpeed;
_speechBubble = GetOrAdd<PrototypeSpeechBubble>();
_voiceCue = GetOrAdd<PrototypeVoiceCue>();
_voicePresenter = GetOrAdd<PrototypeNpcVoicePresenter>();
_gateway = FindAnyObjectByType<SecondSpawnGatewayClient>();
}

Expand Down Expand Up @@ -232,6 +234,30 @@ public void ConfigureCrowdTuning(

public string AgentId => string.IsNullOrWhiteSpace(_agentId) ? name : _agentId.Trim();
public string DisplayName => string.IsNullOrWhiteSpace(_displayName) ? name : _displayName.Trim();
public string VoicePresentationMode => _voicePresenter != null ? _voicePresenter.LastPresentationMode : "none";
public string VoicePresentationReason => _voicePresenter != null ? _voicePresenter.LastVoiceReason : "";
public string FacialTargetSummary => _voicePresenter != null ? _voicePresenter.FacialTargetSummary : "voice_presenter=missing";

public static PrototypeAgentBrain FindActiveByAgentId(string actorId)
{
if (string.IsNullOrWhiteSpace(actorId))
{
return null;
}

var normalized = actorId.Trim();
for (var i = 0; i < ActiveBrains.Count; i++)
{
var brain = ActiveBrains[i];
if (brain != null &&
string.Equals(brain.AgentId, normalized, System.StringComparison.OrdinalIgnoreCase))
{
return brain;
}
}

return null;
}

public void NotifyNearbyPlayerChat(
string message,
Expand Down Expand Up @@ -1366,7 +1392,7 @@ private IEnumerator ApplyDecision(AgentDecisionDto decision, AgentDecisionReques
{
_speechBubble.Show(text);
}
_voiceCue.PlayCue(text);
PresentNpcSpeech(text, request.world_snapshot?.conversation_session_id);
_intentDriver?.TryPlay(CharacterActionId.Talk);
RememberSpeech(text);
_nextTalkAt = Time.time + Mathf.Max(2f, _talkIntervalSeconds);
Expand Down Expand Up @@ -1396,6 +1422,20 @@ private IEnumerator ApplyDecision(AgentDecisionDto decision, AgentDecisionReques
yield break;
}

private void PresentNpcSpeech(string text, string conversationSessionId)
{
if (_voicePresenter != null)
{
_voicePresenter.PresentSpeech(AgentId, conversationSessionId, text, _gateway);
return;
}

if (_voiceCue != null)
{
_voiceCue.PlayCue(text);
}
}

private static bool IsModelDecisionSource(string source)
{
return string.Equals(source, "model", System.StringComparison.OrdinalIgnoreCase) ||
Expand Down
Loading
Loading