commit
53bd68e36d
3
assets/chat_bubble.blend
Normal file
3
assets/chat_bubble.blend
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c5fa5f006a42d87f43a8b411e4e4bf64a8b4fbbaedd0d02579134a8fa59161eb
|
||||||
|
size 894176
|
BIN
assets/chat_bubble.blend1
Normal file
BIN
assets/chat_bubble.blend1
Normal file
Binary file not shown.
3
assets/models/chat_bubble/chat_bubble.glb
Normal file
3
assets/models/chat_bubble/chat_bubble.glb
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f215158ae0aba0113e0077040342fc1b508cfec0a3a1e022c9ce0c16973e2ee1
|
||||||
|
size 17828
|
34
assets/models/chat_bubble/chat_bubble.glb.import
Normal file
34
assets/models/chat_bubble/chat_bubble.glb.import
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://b12raorbby1xd"
|
||||||
|
path="res://.godot/imported/chat_bubble.glb-03622c64b96f5698360bcfb8a4904483.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/models/chat_bubble/chat_bubble.glb"
|
||||||
|
dest_files=["res://.godot/imported/chat_bubble.glb-03622c64b96f5698360bcfb8a4904483.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
import_script/path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=1
|
||||||
|
gltf/embedded_image_handling=1
|
|
@ -2,6 +2,7 @@ extends Node3D
|
||||||
|
|
||||||
var sky = preload ("res://assets/materials/sky.material")
|
var sky = preload ("res://assets/materials/sky.material")
|
||||||
var sky_passthrough = preload ("res://assets/materials/sky_passthrough.material")
|
var sky_passthrough = preload ("res://assets/materials/sky_passthrough.material")
|
||||||
|
const VoiceAssistant = preload ("res://content/system/assist/assist.tscn")
|
||||||
|
|
||||||
@onready var environment: WorldEnvironment = $WorldEnvironment
|
@onready var environment: WorldEnvironment = $WorldEnvironment
|
||||||
@onready var camera: XRCamera3D = $XROrigin3D/XRCamera3D
|
@onready var camera: XRCamera3D = $XROrigin3D/XRCamera3D
|
||||||
|
@ -10,6 +11,7 @@ var sky_passthrough = preload ("res://assets/materials/sky_passthrough.material"
|
||||||
@onready var house = $House
|
@onready var house = $House
|
||||||
@onready var menu = $Menu
|
@onready var menu = $Menu
|
||||||
@onready var keyboard = $Keyboard
|
@onready var keyboard = $Keyboard
|
||||||
|
var voice_assistant = null
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
# In case we're running on the headset, use the passthrough sky
|
# In case we're running on the headset, use the passthrough sky
|
||||||
|
@ -20,6 +22,8 @@ func _ready():
|
||||||
else:
|
else:
|
||||||
RenderingServer.set_debug_generate_wireframes(true)
|
RenderingServer.set_debug_generate_wireframes(true)
|
||||||
|
|
||||||
|
update_voice_assistant()
|
||||||
|
|
||||||
controller_left.button_pressed.connect(func(name):
|
controller_left.button_pressed.connect(func(name):
|
||||||
_emit_action(name, true, false)
|
_emit_action(name, true, false)
|
||||||
)
|
)
|
||||||
|
@ -61,6 +65,17 @@ func _ready():
|
||||||
remove_child(keyboard)
|
remove_child(keyboard)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func update_voice_assistant():
|
||||||
|
if Store.settings.is_loaded() == false:
|
||||||
|
await Store.settings.on_loaded
|
||||||
|
|
||||||
|
if Store.settings.voice_assistant&&voice_assistant == null:
|
||||||
|
voice_assistant = VoiceAssistant.instantiate()
|
||||||
|
add_child(voice_assistant)
|
||||||
|
elif !Store.settings.voice_assistant&&voice_assistant != null:
|
||||||
|
remove_child(voice_assistant)
|
||||||
|
voice_assistant.queue_free()
|
||||||
|
|
||||||
func toggle_menu():
|
func toggle_menu():
|
||||||
if menu.show_menu == false:
|
if menu.show_menu == false:
|
||||||
add_child(menu)
|
add_child(menu)
|
||||||
|
|
109
content/system/assist/assist.gd
Normal file
109
content/system/assist/assist.gd
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
const sample_hold = preload ("res://lib/utils/sample_hold.gd")
|
||||||
|
const Chat = preload ("./chat.gd")
|
||||||
|
|
||||||
|
const audio_freq = 44100
|
||||||
|
const target_freq = 16000
|
||||||
|
const sample_rate_ratio: float = audio_freq / target_freq * 1.5
|
||||||
|
|
||||||
|
var effect: AudioEffectCapture
|
||||||
|
@export var input_threshold: float = 0.05
|
||||||
|
@onready var audio_recorder: AudioStreamPlayer = $AudioStreamRecord
|
||||||
|
@onready var audio_timer: Timer = $AudioTimer
|
||||||
|
@onready var visual_timer: Timer = $VisualTimer
|
||||||
|
@onready var audio_player_3d: AudioStreamPlayer3D = $AudioStreamPlayer3D
|
||||||
|
@onready var chat_user: Chat = $ChatUser
|
||||||
|
@onready var chat_assistant: Chat = $ChatAssistant
|
||||||
|
@onready var loader: Node3D = $Loader
|
||||||
|
@onready var camera = $"/root/Main/XROrigin3D/XRCamera3D"
|
||||||
|
|
||||||
|
var running := false
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
var index = AudioServer.get_bus_index("Record")
|
||||||
|
effect = AudioServer.get_bus_effect(index, 0)
|
||||||
|
|
||||||
|
finish()
|
||||||
|
|
||||||
|
audio_timer.timeout.connect(func():
|
||||||
|
HomeApi.api.assist_handler.send_data(PackedByteArray())
|
||||||
|
)
|
||||||
|
|
||||||
|
HomeApi.api.assist_handler.on_wake_word.connect(func(text):
|
||||||
|
loader.visible=true
|
||||||
|
chat_user.visible=false
|
||||||
|
chat_assistant.visible=false
|
||||||
|
global_position=camera.global_position + camera.global_transform.basis.z * - 0.5
|
||||||
|
global_position.y *= 0.7
|
||||||
|
global_transform.basis=Basis.looking_at((camera.global_position - global_position) * - 1)
|
||||||
|
running=true
|
||||||
|
)
|
||||||
|
|
||||||
|
HomeApi.api.assist_handler.on_stt_message.connect(func(text):
|
||||||
|
loader.visible=false
|
||||||
|
chat_user.visible=true
|
||||||
|
chat_user.text=text
|
||||||
|
)
|
||||||
|
HomeApi.api.assist_handler.on_tts_message.connect(func(text):
|
||||||
|
chat_assistant.visible=true
|
||||||
|
chat_assistant.text=text
|
||||||
|
)
|
||||||
|
|
||||||
|
HomeApi.api.assist_handler.on_tts_sound.connect(func(audio):
|
||||||
|
audio_player_3d.stream=audio
|
||||||
|
audio_player_3d.play()
|
||||||
|
visual_timer.start()
|
||||||
|
running=false
|
||||||
|
)
|
||||||
|
|
||||||
|
HomeApi.api.assist_handler.on_error.connect(func():
|
||||||
|
running=false
|
||||||
|
finish()
|
||||||
|
)
|
||||||
|
|
||||||
|
visual_timer.timeout.connect(func():
|
||||||
|
if audio_player_3d.playing == false:
|
||||||
|
finish()
|
||||||
|
else:
|
||||||
|
await audio_player_3d.finished
|
||||||
|
finish()
|
||||||
|
)
|
||||||
|
|
||||||
|
func finish():
|
||||||
|
if running:
|
||||||
|
return
|
||||||
|
|
||||||
|
chat_user.visible = false
|
||||||
|
chat_assistant.visible = false
|
||||||
|
loader.visible = false
|
||||||
|
|
||||||
|
func _process(_delta):
|
||||||
|
var sterioData: PackedVector2Array = effect.get_buffer(effect.get_frames_available())
|
||||||
|
|
||||||
|
if sterioData.size() == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
var monoSampled := sample_hold.sample_and_hold(sterioData, sample_rate_ratio)
|
||||||
|
|
||||||
|
# 16 bit PCM
|
||||||
|
var data := PackedByteArray()
|
||||||
|
data.resize(monoSampled.size() * 2)
|
||||||
|
|
||||||
|
var max_amplitude = 0.0
|
||||||
|
|
||||||
|
for i in range(monoSampled.size()):
|
||||||
|
|
||||||
|
var value = monoSampled[i]
|
||||||
|
max_amplitude = max(max_amplitude, value)
|
||||||
|
|
||||||
|
data.encode_s16(i * 2, int(value * 32767))
|
||||||
|
|
||||||
|
if max_amplitude > input_threshold:
|
||||||
|
if audio_timer.is_stopped():
|
||||||
|
HomeApi.api.assist_handler.start_wakeword()
|
||||||
|
|
||||||
|
audio_timer.start()
|
||||||
|
|
||||||
|
if audio_timer.is_stopped() == false:
|
||||||
|
HomeApi.api.assist_handler.send_data(data)
|
35
content/system/assist/assist.tscn
Normal file
35
content/system/assist/assist.tscn
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
[gd_scene load_steps=5 format=3 uid="uid://oydbwnek6xb4"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://content/system/assist/assist.gd" id="1_5obhy"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cy6jklyde3pgo" path="res://content/system/assist/chat.tscn" id="2_laew1"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://b0d1582vpkr8m" path="res://content/system/assist/loader.tscn" id="3_25iy1"]
|
||||||
|
|
||||||
|
[sub_resource type="AudioStreamMicrophone" id="AudioStreamMicrophone_6tv2x"]
|
||||||
|
|
||||||
|
[node name="Assist" type="Node3D"]
|
||||||
|
script = ExtResource("1_5obhy")
|
||||||
|
|
||||||
|
[node name="AudioStreamRecord" type="AudioStreamPlayer" parent="."]
|
||||||
|
stream = SubResource("AudioStreamMicrophone_6tv2x")
|
||||||
|
autoplay = true
|
||||||
|
bus = &"Record"
|
||||||
|
|
||||||
|
[node name="AudioTimer" type="Timer" parent="."]
|
||||||
|
wait_time = 2.0
|
||||||
|
one_shot = true
|
||||||
|
|
||||||
|
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="."]
|
||||||
|
|
||||||
|
[node name="VisualTimer" type="Timer" parent="."]
|
||||||
|
wait_time = 5.0
|
||||||
|
one_shot = true
|
||||||
|
|
||||||
|
[node name="ChatUser" parent="." instance=ExtResource("2_laew1")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.109997, 0.025, 0)
|
||||||
|
flip = false
|
||||||
|
|
||||||
|
[node name="ChatAssistant" parent="." instance=ExtResource("2_laew1")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0499932, -0.025, 0)
|
||||||
|
text = "Hello, World!"
|
||||||
|
|
||||||
|
[node name="Loader" parent="." instance=ExtResource("3_25iy1")]
|
37
content/system/assist/chat.gd
Normal file
37
content/system/assist/chat.gd
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
@tool
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
const FontTools = preload ("res://lib/utils/font_tools.gd")
|
||||||
|
|
||||||
|
@onready var label: Label3D = $Label3D
|
||||||
|
@onready var chat: Skeleton3D = $chat_bubble/Armature/Skeleton3D
|
||||||
|
@onready var model: MeshInstance3D = $chat_bubble/Armature/Skeleton3D/Cube
|
||||||
|
|
||||||
|
@export var text := "Hello, World!":
|
||||||
|
set(value):
|
||||||
|
if !is_node_ready(): await ready
|
||||||
|
|
||||||
|
text = value
|
||||||
|
label.text = value
|
||||||
|
update()
|
||||||
|
|
||||||
|
@export var flip: bool = false:
|
||||||
|
set(value):
|
||||||
|
if !is_node_ready(): await ready
|
||||||
|
|
||||||
|
flip = value
|
||||||
|
model.rotation_degrees.x = -90 if value else 90
|
||||||
|
|
||||||
|
const base_width = 0.8 * 0.2
|
||||||
|
|
||||||
|
func update():
|
||||||
|
var text_width = FontTools.get_font_size(label).x
|
||||||
|
|
||||||
|
var offset = (text_width - base_width) / 0.2
|
||||||
|
|
||||||
|
offset = max(0.0, offset)
|
||||||
|
|
||||||
|
if flip:
|
||||||
|
offset = -offset
|
||||||
|
|
||||||
|
chat.set_bone_pose_position(1 if flip else 0, Vector3(0, offset, 0))
|
33
content/system/assist/chat.tscn
Normal file
33
content/system/assist/chat.tscn
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
[gd_scene load_steps=5 format=3 uid="uid://cy6jklyde3pgo"]
|
||||||
|
|
||||||
|
[ext_resource type="PackedScene" uid="uid://b12raorbby1xd" path="res://assets/models/chat_bubble/chat_bubble.glb" id="1_lsdcs"]
|
||||||
|
[ext_resource type="Script" path="res://content/system/assist/chat.gd" id="1_rbrak"]
|
||||||
|
[ext_resource type="Material" uid="uid://bujy3egn1oqac" path="res://assets/materials/pri-500.material" id="2_ps3pl"]
|
||||||
|
[ext_resource type="FontVariation" uid="uid://d2ofyimg5s65q" path="res://assets/fonts/ui_font_500.tres" id="4_gxfp3"]
|
||||||
|
|
||||||
|
[node name="Chat" type="Node3D"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3.41237e-06, 0, 0)
|
||||||
|
script = ExtResource("1_rbrak")
|
||||||
|
text = "Hello World"
|
||||||
|
flip = true
|
||||||
|
|
||||||
|
[node name="chat_bubble" parent="." instance=ExtResource("1_lsdcs")]
|
||||||
|
transform = Transform3D(0.2, 0, 0, 0, 0.2, 0, 0, 0, 0.2, -0.0154175, 0, 0.0710473)
|
||||||
|
|
||||||
|
[node name="Armature" parent="chat_bubble" index="0"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0, 1, 0, -1, 0, 0.5, 0, 0)
|
||||||
|
|
||||||
|
[node name="Cube" parent="chat_bubble/Armature/Skeleton3D" index="0"]
|
||||||
|
transform = Transform3D(-4.37114e-08, -1, -4.37114e-08, 0, -4.37114e-08, 1, -1, 4.37114e-08, 1.91069e-15, 0, 0.35, 0)
|
||||||
|
material_override = ExtResource("2_ps3pl")
|
||||||
|
|
||||||
|
[node name="Label3D" type="Label3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.006)
|
||||||
|
pixel_size = 0.001
|
||||||
|
text = "Hello World"
|
||||||
|
font = ExtResource("4_gxfp3")
|
||||||
|
font_size = 20
|
||||||
|
outline_size = 0
|
||||||
|
horizontal_alignment = 0
|
||||||
|
|
||||||
|
[editable path="chat_bubble"]
|
42
content/system/assist/loader.gd
Normal file
42
content/system/assist/loader.gd
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
@tool
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
const material: StandardMaterial3D = preload ("res://assets/materials/pri-500.material")
|
||||||
|
var time: float = 0.0
|
||||||
|
const DOT_COUNT = 8
|
||||||
|
const RADIUS = 0.025
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
generate_meshes()
|
||||||
|
|
||||||
|
func generate_meshes():
|
||||||
|
for i in range(DOT_COUNT):
|
||||||
|
var mesh := MeshInstance3D.new()
|
||||||
|
mesh.mesh = CylinderMesh.new()
|
||||||
|
mesh.mesh.top_radius = 0.005
|
||||||
|
mesh.mesh.bottom_radius = 0.005
|
||||||
|
mesh.mesh.height = 0.005
|
||||||
|
mesh.material_override = material.duplicate()
|
||||||
|
mesh.material_override.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
|
||||||
|
add_child(mesh)
|
||||||
|
|
||||||
|
mesh.position = Vector3(sin(i * PI / DOT_COUNT * 2), cos(i * PI / DOT_COUNT * 2), 0) * RADIUS
|
||||||
|
mesh.rotation_degrees = Vector3(90, 0, 0)
|
||||||
|
|
||||||
|
func _process(delta):
|
||||||
|
if !visible:
|
||||||
|
return
|
||||||
|
|
||||||
|
time += delta
|
||||||
|
|
||||||
|
for i in range(get_child_count()):
|
||||||
|
var mesh := get_child(i)
|
||||||
|
|
||||||
|
if mesh == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
mesh.material_override.albedo_color.a = saw_tooth(i / float(get_child_count()) + time)
|
||||||
|
|
||||||
|
func saw_tooth(x: float) -> float:
|
||||||
|
return 1 - fmod(x, 1)
|
6
content/system/assist/loader.tscn
Normal file
6
content/system/assist/loader.tscn
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3 uid="uid://b0d1582vpkr8m"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://content/system/assist/loader.gd" id="1_3bi3s"]
|
||||||
|
|
||||||
|
[node name="Loader" type="Node3D"]
|
||||||
|
script = ExtResource("1_3bi3s")
|
|
@ -1,5 +1,7 @@
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
|
const FontTools = preload ("res://lib/utils/font_tools.gd")
|
||||||
|
|
||||||
var label: Label3D
|
var label: Label3D
|
||||||
|
|
||||||
var text: String = ""
|
var text: String = ""
|
||||||
|
@ -73,14 +75,13 @@ func _calculate_caret_position(click_pos_x: float):
|
||||||
return gap_offsets.size() - 1
|
return gap_offsets.size() - 1
|
||||||
|
|
||||||
func _calculate_text_gaps():
|
func _calculate_text_gaps():
|
||||||
var font = label.get_font()
|
|
||||||
var offsets = [0.0]
|
var offsets = [0.0]
|
||||||
|
|
||||||
for i in range(text.length()):
|
for i in range(text.length()):
|
||||||
var chars = text.substr(0, i + 1) # Can't use single chars because of kerning.
|
var chars = text.substr(0, i + 1) # Can't use single chars because of kerning.
|
||||||
var size = font.get_string_size(chars, HORIZONTAL_ALIGNMENT_CENTER, -1, label.font_size)
|
var size = FontTools.get_font_size(label, chars)
|
||||||
|
|
||||||
offsets.append(size.x * label.pixel_size)
|
offsets.append(size.x)
|
||||||
|
|
||||||
return offsets
|
return offsets
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ extends Node3D
|
||||||
const credits_scene = preload ("./credits.tscn")
|
const credits_scene = preload ("./credits.tscn")
|
||||||
|
|
||||||
@onready var connection_status = $Content/ConnectionStatus
|
@onready var connection_status = $Content/ConnectionStatus
|
||||||
|
@onready var main = $"/root/Main"
|
||||||
|
|
||||||
@onready var input_url = $Content/InputURL
|
@onready var input_url = $Content/InputURL
|
||||||
@onready var input_token = $Content/InputToken
|
@onready var input_token = $Content/InputToken
|
||||||
|
@ -11,6 +12,7 @@ const credits_scene = preload ("./credits.tscn")
|
||||||
@onready var save = $Content/Save
|
@onready var save = $Content/Save
|
||||||
@onready var clear_save = $Content/ClearSave
|
@onready var clear_save = $Content/ClearSave
|
||||||
@onready var background = $Background
|
@onready var background = $Background
|
||||||
|
@onready var voice_assist = $Content/VoiceAssist
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
background.visible = false
|
background.visible = false
|
||||||
|
@ -53,6 +55,31 @@ func _ready():
|
||||||
House.body.update_house()
|
House.body.update_house()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
voice_assist.on_button_down.connect(func():
|
||||||
|
if Store.settings.is_loaded() == false:
|
||||||
|
await Store.settings.on_loaded
|
||||||
|
|
||||||
|
OS.request_permissions()
|
||||||
|
|
||||||
|
voice_assist.label="mic"
|
||||||
|
|
||||||
|
Store.settings.voice_assistant=true
|
||||||
|
main.update_voice_assistant()
|
||||||
|
Store.settings.save_local()
|
||||||
|
)
|
||||||
|
|
||||||
|
voice_assist.on_button_up.connect(func():
|
||||||
|
if Store.settings.is_loaded() == false:
|
||||||
|
await Store.settings.on_loaded
|
||||||
|
|
||||||
|
voice_assist.label="mic_off"
|
||||||
|
|
||||||
|
Store.settings.voice_assistant=false
|
||||||
|
main.update_voice_assistant()
|
||||||
|
Store.settings.save_local()
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
HomeApi.on_connect.connect(func():
|
HomeApi.on_connect.connect(func():
|
||||||
connection_status.text="Connected"
|
connection_status.text="Connected"
|
||||||
)
|
)
|
||||||
|
@ -60,3 +87,9 @@ func _ready():
|
||||||
HomeApi.on_disconnect.connect(func():
|
HomeApi.on_disconnect.connect(func():
|
||||||
connection_status.text="Disconnected"
|
connection_status.text="Disconnected"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if Store.settings.is_loaded() == false:
|
||||||
|
await Store.settings.on_loaded
|
||||||
|
|
||||||
|
voice_assist.label = "mic_off" if Store.settings.voice_assistant == false else "mic"
|
||||||
|
voice_assist.active = Store.settings.voice_assistant
|
||||||
|
|
|
@ -131,3 +131,18 @@ outline_size = 0
|
||||||
horizontal_alignment = 0
|
horizontal_alignment = 0
|
||||||
autowrap_mode = 3
|
autowrap_mode = 3
|
||||||
width = 150.0
|
width = 150.0
|
||||||
|
|
||||||
|
[node name="VoiceAssist" parent="Content" instance=ExtResource("1_faxng")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.1, 0, 0.12)
|
||||||
|
label = "mic_off"
|
||||||
|
icon = true
|
||||||
|
toggleable = true
|
||||||
|
|
||||||
|
[node name="LabelVoiceAssist" type="Label3D" parent="Content"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0.01, 0, 0.12)
|
||||||
|
pixel_size = 0.001
|
||||||
|
text = "Voice-
|
||||||
|
Assist:"
|
||||||
|
font_size = 18
|
||||||
|
outline_size = 0
|
||||||
|
horizontal_alignment = 0
|
||||||
|
|
3
default_bus_layout.tres
Normal file
3
default_bus_layout.tres
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:fb9d247646174775b00db7902c224ac62f734b3a6467af32919d12d2a6861c38
|
||||||
|
size 555
|
|
@ -155,7 +155,7 @@ permissions/receive_boot_completed=false
|
||||||
permissions/receive_mms=false
|
permissions/receive_mms=false
|
||||||
permissions/receive_sms=false
|
permissions/receive_sms=false
|
||||||
permissions/receive_wap_push=false
|
permissions/receive_wap_push=false
|
||||||
permissions/record_audio=false
|
permissions/record_audio=true
|
||||||
permissions/reorder_tasks=false
|
permissions/reorder_tasks=false
|
||||||
permissions/restart_packages=false
|
permissions/restart_packages=false
|
||||||
permissions/send_respond_via_message=false
|
permissions/send_respond_via_message=false
|
||||||
|
@ -377,7 +377,7 @@ permissions/receive_boot_completed=false
|
||||||
permissions/receive_mms=false
|
permissions/receive_mms=false
|
||||||
permissions/receive_sms=false
|
permissions/receive_sms=false
|
||||||
permissions/receive_wap_push=false
|
permissions/receive_wap_push=false
|
||||||
permissions/record_audio=false
|
permissions/record_audio=true
|
||||||
permissions/reorder_tasks=false
|
permissions/reorder_tasks=false
|
||||||
permissions/restart_packages=false
|
permissions/restart_packages=false
|
||||||
permissions/send_respond_via_message=false
|
permissions/send_respond_via_message=false
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
@onready var body = get_node("/root/Main/House")
|
@onready var body = get_node_or_null("/root/Main/House")
|
132
lib/home_apis/hass_ws/handlers/assist.gd
Normal file
132
lib/home_apis/hass_ws/handlers/assist.gd
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
const HASS_API = preload ("../hass.gd")
|
||||||
|
|
||||||
|
signal on_wake_word(wake_word: String)
|
||||||
|
signal on_stt_message(message: String)
|
||||||
|
signal on_tts_message(message: String)
|
||||||
|
signal on_tts_sound(sound: AudioStreamMP3)
|
||||||
|
signal on_error()
|
||||||
|
|
||||||
|
var api: HASS_API
|
||||||
|
var pipe_running := false
|
||||||
|
var handler_id := 0
|
||||||
|
var wake_word = null:
|
||||||
|
set(value):
|
||||||
|
if value != wake_word&&value != null:
|
||||||
|
on_wake_word.emit(value)
|
||||||
|
wake_word = value
|
||||||
|
|
||||||
|
var stt_message = null:
|
||||||
|
set(value):
|
||||||
|
if value != stt_message&&value != null:
|
||||||
|
on_stt_message.emit(value)
|
||||||
|
stt_message = value
|
||||||
|
|
||||||
|
var tts_message = null:
|
||||||
|
set(value):
|
||||||
|
if value != tts_message&&value != null:
|
||||||
|
on_tts_message.emit(value)
|
||||||
|
tts_message = value
|
||||||
|
|
||||||
|
var tts_sound = null:
|
||||||
|
set(value):
|
||||||
|
if value != tts_sound&&value != null:
|
||||||
|
on_tts_sound.emit(value)
|
||||||
|
tts_sound = value
|
||||||
|
|
||||||
|
func _init(hass: HASS_API):
|
||||||
|
self.api = hass
|
||||||
|
|
||||||
|
func on_connect():
|
||||||
|
pass
|
||||||
|
|
||||||
|
func start_wakeword():
|
||||||
|
if pipe_running:
|
||||||
|
return
|
||||||
|
|
||||||
|
api.send_packet({
|
||||||
|
"type": "assist_pipeline/run",
|
||||||
|
"start_stage": "wake_word",
|
||||||
|
"end_stage": "tts",
|
||||||
|
"input": {
|
||||||
|
"timeout": 5,
|
||||||
|
"sample_rate": 16000
|
||||||
|
},
|
||||||
|
"timeout": 60
|
||||||
|
}, true)
|
||||||
|
|
||||||
|
func send_data(data: PackedByteArray):
|
||||||
|
|
||||||
|
# prepend the handler id to the data in 8 bits
|
||||||
|
if pipe_running:
|
||||||
|
var stream = PackedByteArray()
|
||||||
|
|
||||||
|
stream.resize(1)
|
||||||
|
stream.encode_s8(0, handler_id)
|
||||||
|
stream.append_array(data)
|
||||||
|
|
||||||
|
api.send_raw(stream)
|
||||||
|
|
||||||
|
func handle_message(message: Dictionary):
|
||||||
|
if message["type"] != "event":
|
||||||
|
return
|
||||||
|
|
||||||
|
var event = message["event"]
|
||||||
|
|
||||||
|
if event.has("type") == false:
|
||||||
|
return
|
||||||
|
|
||||||
|
match event["type"]:
|
||||||
|
"run-start":
|
||||||
|
pipe_running = true
|
||||||
|
handler_id = event["data"]["runner_data"]["stt_binary_handler_id"]
|
||||||
|
"wake_word-end":
|
||||||
|
if pipe_running == false:
|
||||||
|
return
|
||||||
|
|
||||||
|
if event["data"]["wake_word_output"].has("wake_word_phrase") == false:
|
||||||
|
return
|
||||||
|
|
||||||
|
wake_word = event["data"]["wake_word_output"]["wake_word_phrase"]
|
||||||
|
"stt-end":
|
||||||
|
if pipe_running == false:
|
||||||
|
return
|
||||||
|
|
||||||
|
if event["data"]["stt_output"].has("text") == false:
|
||||||
|
return
|
||||||
|
|
||||||
|
stt_message = event["data"]["stt_output"]["text"]
|
||||||
|
"intent-end":
|
||||||
|
if pipe_running == false:
|
||||||
|
return
|
||||||
|
|
||||||
|
tts_message = event["data"]["intent_output"]["response"]["speech"]["plain"]["speech"]
|
||||||
|
"tts-end":
|
||||||
|
if pipe_running == false:
|
||||||
|
return
|
||||||
|
|
||||||
|
if event["data"]["tts_output"].has("url") == false:
|
||||||
|
return
|
||||||
|
|
||||||
|
var headers = PackedStringArray(["Authorization: Bearer %s" % api.token, "Content-Type: application/json"])
|
||||||
|
var url = "%s://%s%s" % ["https" if api.url.begins_with("wss") else "http", api.url.split("//")[1],event["data"]["tts_output"]["url"]]
|
||||||
|
|
||||||
|
Request.request(url, headers, HTTPClient.METHOD_GET)
|
||||||
|
|
||||||
|
var response = await Request.request_completed
|
||||||
|
|
||||||
|
if response[0] != HTTPRequest.RESULT_SUCCESS:
|
||||||
|
return
|
||||||
|
|
||||||
|
var sound = AudioStreamMP3.new()
|
||||||
|
sound.data = response[3]
|
||||||
|
|
||||||
|
tts_sound = sound
|
||||||
|
"error":
|
||||||
|
if event["data"]["code"] == "stt-no-text-recognized":
|
||||||
|
on_error.emit()
|
||||||
|
"run-end":
|
||||||
|
pipe_running = false
|
||||||
|
wake_word = null
|
||||||
|
handler_id = 0
|
||||||
|
_:
|
||||||
|
pass
|
|
@ -2,6 +2,7 @@ extends Node
|
||||||
|
|
||||||
const AuthHandler = preload ("./handlers/auth.gd")
|
const AuthHandler = preload ("./handlers/auth.gd")
|
||||||
const IntegrationHandler = preload ("./handlers/integration.gd")
|
const IntegrationHandler = preload ("./handlers/integration.gd")
|
||||||
|
const AssistHandler = preload ("./handlers/assist.gd")
|
||||||
|
|
||||||
signal on_connect()
|
signal on_connect()
|
||||||
signal on_disconnect()
|
signal on_disconnect()
|
||||||
|
@ -25,6 +26,7 @@ var packet_callbacks := CallbackMap.new()
|
||||||
|
|
||||||
var auth_handler: AuthHandler
|
var auth_handler: AuthHandler
|
||||||
var integration_handler: IntegrationHandler
|
var integration_handler: IntegrationHandler
|
||||||
|
var assist_handler: AssistHandler
|
||||||
|
|
||||||
func _init(url:=self.url, token:=self.token):
|
func _init(url:=self.url, token:=self.token):
|
||||||
self.url = url
|
self.url = url
|
||||||
|
@ -32,6 +34,7 @@ func _init(url:=self.url, token:=self.token):
|
||||||
|
|
||||||
auth_handler = AuthHandler.new(self, url, token)
|
auth_handler = AuthHandler.new(self, url, token)
|
||||||
integration_handler = IntegrationHandler.new(self)
|
integration_handler = IntegrationHandler.new(self)
|
||||||
|
assist_handler = AssistHandler.new(self)
|
||||||
|
|
||||||
devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ")
|
devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ")
|
||||||
connect_ws()
|
connect_ws()
|
||||||
|
@ -82,6 +85,7 @@ func handle_packet(packet: Dictionary):
|
||||||
if LOG_MESSAGES: print("Received packet: %s" % str(packet).substr(0, 1000))
|
if LOG_MESSAGES: print("Received packet: %s" % str(packet).substr(0, 1000))
|
||||||
|
|
||||||
auth_handler.handle_message(packet)
|
auth_handler.handle_message(packet)
|
||||||
|
assist_handler.handle_message(packet)
|
||||||
|
|
||||||
if packet.has("id"):
|
if packet.has("id"):
|
||||||
packet_callbacks.call_key(int(packet.id), [packet])
|
packet_callbacks.call_key(int(packet.id), [packet])
|
||||||
|
@ -117,6 +121,7 @@ func start_subscriptions():
|
||||||
|
|
||||||
func handle_connect():
|
func handle_connect():
|
||||||
integration_handler.on_connect()
|
integration_handler.on_connect()
|
||||||
|
assist_handler.on_connect()
|
||||||
connected = true
|
connected = true
|
||||||
on_connect.emit()
|
on_connect.emit()
|
||||||
|
|
||||||
|
@ -176,7 +181,15 @@ func send_request_packet(packet: Dictionary, ignore_initial:=false):
|
||||||
|
|
||||||
return await promise.settled
|
return await promise.settled
|
||||||
|
|
||||||
func send_packet(packet: Dictionary):
|
func send_raw(packet: PackedByteArray):
|
||||||
|
if LOG_MESSAGES: print("Sending binary: %s" % packet.hex_encode())
|
||||||
|
socket.send(packet)
|
||||||
|
|
||||||
|
func send_packet(packet: Dictionary, with_id:=false):
|
||||||
|
if with_id:
|
||||||
|
packet.id = id
|
||||||
|
id += 1
|
||||||
|
|
||||||
if LOG_MESSAGES: print("Sending packet: %s" % encode_packet(packet))
|
if LOG_MESSAGES: print("Sending packet: %s" % encode_packet(packet))
|
||||||
socket.send_text(encode_packet(packet))
|
socket.send_text(encode_packet(packet))
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ extends StoreClass
|
||||||
|
|
||||||
const StoreClass = preload ("./store.gd")
|
const StoreClass = preload ("./store.gd")
|
||||||
|
|
||||||
|
|
||||||
var type: String = "HASS_WS"
|
var type: String = "HASS_WS"
|
||||||
var url: String = ""
|
var url: String = ""
|
||||||
var token: String = ""
|
var token: String = ""
|
||||||
|
var voice_assistant: bool = false
|
||||||
|
|
||||||
func _init():
|
func _init():
|
||||||
_save_path = "user://settings.json"
|
_save_path = "user://settings.json"
|
||||||
|
@ -14,3 +14,4 @@ func clear():
|
||||||
type = "HASS_WS"
|
type = "HASS_WS"
|
||||||
url = ""
|
url = ""
|
||||||
token = ""
|
token = ""
|
||||||
|
voice_assistant = false
|
|
@ -36,6 +36,10 @@ func use_dict(dict: Dictionary):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
var prop = get(prop_info.name)
|
var prop = get(prop_info.name)
|
||||||
|
|
||||||
|
if dict.has(prop_info.name) == false:
|
||||||
|
continue
|
||||||
|
|
||||||
var prop_value = dict[prop_info.name]
|
var prop_value = dict[prop_info.name]
|
||||||
|
|
||||||
if prop is Store:
|
if prop is Store:
|
||||||
|
|
9
lib/utils/font_tools.gd
Normal file
9
lib/utils/font_tools.gd
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
static func get_font_size(label: Label3D, chars=null):
|
||||||
|
var font = label.font
|
||||||
|
|
||||||
|
if font == null:
|
||||||
|
return Vector2(0, 0)
|
||||||
|
|
||||||
|
var size = font.get_string_size(label.text if chars == null else chars, label.horizontal_alignment, label.width, label.font_size) * label.pixel_size
|
||||||
|
|
||||||
|
return size
|
11
lib/utils/sample_hold.gd
Normal file
11
lib/utils/sample_hold.gd
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
static func sample_and_hold(data: PackedVector2Array, sample_rate: float) -> PackedFloat32Array:
|
||||||
|
var new_data: PackedFloat32Array = PackedFloat32Array()
|
||||||
|
new_data.resize(int(data.size() / sample_rate))
|
||||||
|
|
||||||
|
var counter = 0.0
|
||||||
|
|
||||||
|
for i in range(new_data.size()):
|
||||||
|
new_data[i] = data[int(counter)].y
|
||||||
|
counter += sample_rate
|
||||||
|
|
||||||
|
return new_data
|
8
lib/utils/sample_hold.tscn
Normal file
8
lib/utils/sample_hold.tscn
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[gd_scene load_steps=2 format=3 uid="uid://b4l22m7bxamsc"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://test/lib/utils/sample_hold/sample_hold.gd" id="1_t0y35"]
|
||||||
|
|
||||||
|
[node name="Node2D" type="Node2D"]
|
||||||
|
script = ExtResource("1_t0y35")
|
||||||
|
|
||||||
|
[node name="CanvasLayer" type="CanvasLayer" parent="."]
|
|
@ -15,6 +15,10 @@ run/main_scene="res://content/main.tscn"
|
||||||
config/features=PackedStringArray("4.2", "Mobile")
|
config/features=PackedStringArray("4.2", "Mobile")
|
||||||
config/icon="res://assets/logo.png"
|
config/icon="res://assets/logo.png"
|
||||||
|
|
||||||
|
[audio]
|
||||||
|
|
||||||
|
driver/enable_input=true
|
||||||
|
|
||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
XRToolsUserSettings="*res://addons/godot-xr-tools/user_settings/user_settings.gd"
|
XRToolsUserSettings="*res://addons/godot-xr-tools/user_settings/user_settings.gd"
|
||||||
|
|
34
test/lib/utils/sample_hold/sample_hold.gd
Normal file
34
test/lib/utils/sample_hold/sample_hold.gd
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
@tool
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
const sample_hold = preload ("res://lib/utils/sample_hold.gd")
|
||||||
|
|
||||||
|
var data = PackedVector2Array()
|
||||||
|
var result: PackedFloat32Array
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
for i in range(0, 44100):
|
||||||
|
var value = sin(i * 2 * PI / 44100.0)
|
||||||
|
data.push_back(Vector2(value, value))
|
||||||
|
|
||||||
|
result = sample_hold.sample_and_hold(data, 44100.0 / 16000.0 * 1.5)
|
||||||
|
|
||||||
|
func _draw():
|
||||||
|
var size = get_viewport().get_visible_rect().size
|
||||||
|
size.x *= 10
|
||||||
|
size.y *= 4
|
||||||
|
var center = size / 2
|
||||||
|
|
||||||
|
draw_line(Vector2(0, size.y / 2), Vector2(size.x, size.y / 2), Color(1, 1, 1))
|
||||||
|
|
||||||
|
for i in range(0, data.size()):
|
||||||
|
var value = data[i]
|
||||||
|
var x = i * (size.x / data.size())
|
||||||
|
|
||||||
|
draw_line(Vector2(x, 0), Vector2(x, value.x * center.y), Color(1, 0, 0))
|
||||||
|
|
||||||
|
for i in range(0, result.size()):
|
||||||
|
var value = result[i]
|
||||||
|
var x = i * (size.x / result.size())
|
||||||
|
|
||||||
|
draw_line(Vector2(x, 0), Vector2(x, value * center.y), Color(0, 1, 0))
|
3
test/lib/utils/sample_hold/sample_hold.tscn
Normal file
3
test/lib/utils/sample_hold/sample_hold.tscn
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[gd_scene format=3 uid="uid://bpy811vonnq2u"]
|
||||||
|
|
||||||
|
[node name="Node2D" type="Node2D"]
|
Loading…
Reference in New Issue
Block a user