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
4 changes: 2 additions & 2 deletions CODEOWNERS

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

7 changes: 4 additions & 3 deletions homeassistant/components/ai_task/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.helpers import llm
from homeassistant.helpers.chat_session import async_get_chat_session
from homeassistant.helpers.chat_session import ChatSession
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.util import dt as dt_util

Expand Down Expand Up @@ -56,12 +56,12 @@ async def async_internal_added_to_hass(self) -> None:
@contextlib.asynccontextmanager
async def _async_get_ai_task_chat_log(
self,
session: ChatSession,
task: GenDataTask,
) -> AsyncGenerator[ChatLog]:
"""Context manager used to manage the ChatLog used during an AI Task."""
# pylint: disable-next=contextmanager-generator-missing-cleanup
with (
async_get_chat_session(self.hass) as session,
async_get_chat_log(
self.hass,
session,
Expand All @@ -88,12 +88,13 @@ async def _async_get_ai_task_chat_log(
@final
async def internal_async_generate_data(
self,
session: ChatSession,
task: GenDataTask,
) -> GenDataTaskResult:
"""Run a gen data task."""
self.__last_activity = dt_util.utcnow().isoformat()
self.async_write_ha_state()
async with self._async_get_ai_task_chat_log(task) as chat_log:
async with self._async_get_ai_task_chat_log(session, task) as chat_log:
return await self._async_generate_data(task, chat_log)

async def _async_generate_data(
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/ai_task/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"domain": "ai_task",
"name": "AI Task",
"after_dependencies": ["camera"],
"codeowners": ["@home-assistant/core"],
"dependencies": ["conversation", "media_source"],
"documentation": "https://www.home-assistant.io/integrations/ai_task",
Expand Down
95 changes: 74 additions & 21 deletions homeassistant/components/ai_task/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,32 @@
from __future__ import annotations

from dataclasses import dataclass
import mimetypes
from pathlib import Path
import tempfile
from typing import Any

import voluptuous as vol

from homeassistant.components import conversation, media_source
from homeassistant.core import HomeAssistant
from homeassistant.components import camera, conversation, media_source
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.chat_session import async_get_chat_session

from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature


def _save_camera_snapshot(image: camera.Image) -> Path:
"""Save camera snapshot to temp file."""
with tempfile.NamedTemporaryFile(
mode="wb",
suffix=mimetypes.guess_extension(image.content_type, False),
delete=False,
) as temp_file:
temp_file.write(image.content)
return Path(temp_file.name)


async def async_generate_data(
hass: HomeAssistant,
*,
Expand All @@ -40,41 +55,79 @@ async def async_generate_data(
)

# Resolve attachments
resolved_attachments: list[conversation.Attachment] | None = None
resolved_attachments: list[conversation.Attachment] = []
created_files: list[Path] = []

if attachments:
if AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features:
raise HomeAssistantError(
f"AI Task entity {entity_id} does not support attachments"
)
if (
attachments
and AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features
):
raise HomeAssistantError(
f"AI Task entity {entity_id} does not support attachments"
)

for attachment in attachments or []:
media_content_id = attachment["media_content_id"]

resolved_attachments = []
# Special case for camera media sources
if media_content_id.startswith("media-source://camera/"):
# Extract entity_id from the media content ID
entity_id = media_content_id.removeprefix("media-source://camera/")

for attachment in attachments:
media = await media_source.async_resolve_media(
hass, attachment["media_content_id"], None
# Get snapshot from camera
image = await camera.async_get_image(hass, entity_id)

temp_filename = await hass.async_add_executor_job(
_save_camera_snapshot, image
)
created_files.append(temp_filename)

resolved_attachments.append(
conversation.Attachment(
media_content_id=media_content_id,
mime_type=image.content_type,
path=temp_filename,
)
)
else:
# Handle regular media sources
media = await media_source.async_resolve_media(hass, media_content_id, None)
if media.path is None:
raise HomeAssistantError(
"Only local attachments are currently supported"
)
resolved_attachments.append(
conversation.Attachment(
media_content_id=attachment["media_content_id"],
url=media.url,
media_content_id=media_content_id,
mime_type=media.mime_type,
path=media.path,
)
)

return await entity.internal_async_generate_data(
GenDataTask(
name=task_name,
instructions=instructions,
structure=structure,
attachments=resolved_attachments,
with async_get_chat_session(hass) as session:
if created_files:

def cleanup_files() -> None:
"""Cleanup temporary files."""
for file in created_files:
file.unlink(missing_ok=True)

@callback
def cleanup_files_callback() -> None:
"""Cleanup temporary files."""
hass.async_add_executor_job(cleanup_files)

session.async_on_cleanup(cleanup_files_callback)

return await entity.internal_async_generate_data(
session,
GenDataTask(
name=task_name,
instructions=instructions,
structure=structure,
attachments=resolved_attachments or None,
),
)
)


@dataclass(slots=True)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/airzone_cloud/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.6.12"]
"requirements": ["aioairzone-cloud==0.6.13"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/amcrest/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["amcrest"],
"quality_scale": "legacy",
"requirements": ["amcrest==1.9.8"]
"requirements": ["amcrest==1.9.9"]
}
3 changes: 0 additions & 3 deletions homeassistant/components/conversation/chat_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,6 @@ class Attachment:
media_content_id: str
"""Media content ID of the attachment."""

url: str
"""URL of the attachment."""

mime_type: str
"""MIME type of the attachment."""

Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/elevenlabs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@

async def get_model_by_id(client: AsyncElevenLabs, model_id: str) -> Model | None:
"""Get ElevenLabs model from their API by the model_id."""
models = await client.models.get_all()
models = await client.models.list()

for maybe_model in models:
if maybe_model.model_id == model_id:
return maybe_model
Expand Down
20 changes: 9 additions & 11 deletions homeassistant/components/elevenlabs/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@
from .const import (
CONF_CONFIGURE_VOICE,
CONF_MODEL,
CONF_OPTIMIZE_LATENCY,
CONF_SIMILARITY,
CONF_STABILITY,
CONF_STYLE,
CONF_USE_SPEAKER_BOOST,
CONF_VOICE,
DEFAULT_MODEL,
DEFAULT_OPTIMIZE_LATENCY,
DEFAULT_SIMILARITY,
DEFAULT_STABILITY,
DEFAULT_STYLE,
Expand All @@ -51,7 +49,8 @@ async def get_voices_models(
httpx_client = get_async_client(hass)
client = AsyncElevenLabs(api_key=api_key, httpx_client=httpx_client)
voices = (await client.voices.get_all()).voices
models = await client.models.get_all()
models = await client.models.list()

voices_dict = {
voice.voice_id: voice.name
for voice in sorted(voices, key=lambda v: v.name or "")
Expand All @@ -78,8 +77,13 @@ async def async_step_user(
if user_input is not None:
try:
voices, _ = await get_voices_models(self.hass, user_input[CONF_API_KEY])
except ApiError:
errors["base"] = "invalid_api_key"
except ApiError as exc:
errors["base"] = "unknown"
details = getattr(exc, "body", {}).get("detail", {})
if details:
status = details.get("status")
if status == "invalid_api_key":
errors["base"] = "invalid_api_key"
else:
return self.async_create_entry(
title="ElevenLabs",
Expand Down Expand Up @@ -206,12 +210,6 @@ def elevenlabs_config_options_voice_schema(self) -> vol.Schema:
vol.Coerce(float),
vol.Range(min=0, max=1),
),
vol.Optional(
CONF_OPTIMIZE_LATENCY,
default=self.config_entry.options.get(
CONF_OPTIMIZE_LATENCY, DEFAULT_OPTIMIZE_LATENCY
),
): vol.All(int, vol.Range(min=0, max=4)),
vol.Optional(
CONF_STYLE,
default=self.config_entry.options.get(CONF_STYLE, DEFAULT_STYLE),
Expand Down
2 changes: 0 additions & 2 deletions homeassistant/components/elevenlabs/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@
CONF_CONFIGURE_VOICE = "configure_voice"
CONF_STABILITY = "stability"
CONF_SIMILARITY = "similarity"
CONF_OPTIMIZE_LATENCY = "optimize_streaming_latency"
CONF_STYLE = "style"
CONF_USE_SPEAKER_BOOST = "use_speaker_boost"
DOMAIN = "elevenlabs"

DEFAULT_MODEL = "eleven_multilingual_v2"
DEFAULT_STABILITY = 0.5
DEFAULT_SIMILARITY = 0.75
DEFAULT_OPTIMIZE_LATENCY = 0
DEFAULT_STYLE = 0
DEFAULT_USE_SPEAKER_BOOST = True
2 changes: 1 addition & 1 deletion homeassistant/components/elevenlabs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["elevenlabs"],
"requirements": ["elevenlabs==1.9.0"]
"requirements": ["elevenlabs==2.3.0"]
}
5 changes: 2 additions & 3 deletions homeassistant/components/elevenlabs/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
}
},
"error": {
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]"
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"options": {
Expand All @@ -32,14 +33,12 @@
"data": {
"stability": "Stability",
"similarity": "Similarity",
"optimize_streaming_latency": "Latency",
"style": "Style",
"use_speaker_boost": "Speaker boost"
},
"data_description": {
"stability": "Stability of the generated audio. Higher values lead to less emotional audio.",
"similarity": "Similarity of the generated audio to the original voice. Higher values may result in more similar audio, but may also introduce background noise.",
"optimize_streaming_latency": "Optimize the model for streaming. This may reduce the quality of the generated audio.",
"style": "Style of the generated audio. Recommended to keep at 0 for most almost all use cases.",
"use_speaker_boost": "Use speaker boost to increase the similarity of the generated audio to the original voice."
}
Expand Down
15 changes: 4 additions & 11 deletions homeassistant/components/elevenlabs/tts.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,11 @@
from . import ElevenLabsConfigEntry
from .const import (
ATTR_MODEL,
CONF_OPTIMIZE_LATENCY,
CONF_SIMILARITY,
CONF_STABILITY,
CONF_STYLE,
CONF_USE_SPEAKER_BOOST,
CONF_VOICE,
DEFAULT_OPTIMIZE_LATENCY,
DEFAULT_SIMILARITY,
DEFAULT_STABILITY,
DEFAULT_STYLE,
Expand Down Expand Up @@ -75,9 +73,6 @@ async def async_setup_entry(
config_entry.entry_id,
config_entry.title,
voice_settings,
config_entry.options.get(
CONF_OPTIMIZE_LATENCY, DEFAULT_OPTIMIZE_LATENCY
),
)
]
)
Expand All @@ -98,7 +93,6 @@ def __init__(
entry_id: str,
title: str,
voice_settings: VoiceSettings,
latency: int = 0,
) -> None:
"""Init ElevenLabs TTS service."""
self._client = client
Expand All @@ -115,7 +109,6 @@ def __init__(
if voice_indices:
self._voices.insert(0, self._voices.pop(voice_indices[0]))
self._voice_settings = voice_settings
self._latency = latency

# Entity attributes
self._attr_unique_id = entry_id
Expand Down Expand Up @@ -144,14 +137,14 @@ async def async_get_tts_audio(
voice_id = options.get(ATTR_VOICE, self._default_voice_id)
model = options.get(ATTR_MODEL, self._model.model_id)
try:
audio = await self._client.generate(
audio = self._client.text_to_speech.convert(
text=message,
voice=voice_id,
optimize_streaming_latency=self._latency,
voice_id=voice_id,
voice_settings=self._voice_settings,
model=model,
model_id=model,
)
bytes_combined = b"".join([byte_seg async for byte_seg in audio])

except ApiError as exc:
_LOGGER.warning(
"Error during processing of TTS request %s", exc, exc_info=True
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/nasweb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from .coordinator import NASwebCoordinator
from .nasweb_data import NASwebData

PLATFORMS: list[Platform] = [Platform.SWITCH]
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH]

NASWEB_CONFIG_URL = "https://{host}/page"

Expand Down
Loading
Loading