Skip to content
Open
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
1 change: 1 addition & 0 deletions ai_ear/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def main(argv: list[str] | None = None) -> int:
app,
host=args.host,
port=args.port,
workers=settings.api_workers,
reload=args.reload,
log_level="info",
)
Expand Down
1 change: 1 addition & 0 deletions ai_ear/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Settings(BaseSettings):
# ------------------------------------------------------------------ API
api_host: str = Field(default="0.0.0.0", description="API server bind host")
api_port: int = Field(default=8080, ge=1, le=65535, description="API server bind port")
api_workers: int = Field(default=1, ge=1, description="Number of uvicorn worker processes")
api_cors_origins: list[str] = Field(
default=["*"], description="Allowed CORS origins"
)
Expand Down
17 changes: 17 additions & 0 deletions ai_ear/core/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
AudioChunk,
AuralEvent,
AuralEventType,
EmotionLabel,
EnvironmentLabel,
)

Expand Down Expand Up @@ -82,6 +83,7 @@ def __init__(
self._prev_env_by_source: dict[str, EnvironmentLabel | None] = {}
self._prev_music_active_by_source: dict[str, bool] = {}
self._prev_speech_active_by_source: dict[str, bool] = {}
self._prev_emotion_by_source: dict[str, EmotionLabel | None] = {}

# ------------------------------------------------------------------
# Analyser registration
Expand Down Expand Up @@ -330,6 +332,21 @@ def _derive_events(self, result: AnalysisResult) -> list[AuralEvent]:
)
)

# Emotion shift (tracked per source)
emotion_now = result.emotion.dominant if result.emotion else None
prev_emotion = self._prev_emotion_by_source.get(src)
if emotion_now is not None and emotion_now != prev_emotion:
events.append(
AuralEvent(
event_type=AuralEventType.EMOTION_SHIFT,
source_id=src,
description=f"Emotion shifted to '{emotion_now.value}'",
payload={"previous": prev_emotion, "current": emotion_now},
severity=result.emotion.arousal if result.emotion else 0.0,
)
)
self._prev_emotion_by_source[src] = emotion_now

return events

@property
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ All settings can be overridden via environment variables prefixed with `AIEAR_`.
| `AIEAR_ENVIRONMENT_ENABLED` | bool | `true` | Enable environment classification |
| `AIEAR_ENVIRONMENT_NOISE_GATE_DB` | float | `-50.0` | Frames below this dBFS level are classified as silence |
| `AIEAR_MEMORY_MAX_RESULTS` | int | `500` | Rolling buffer size for analysis results |
| `AIEAR_MEMORY_MAX_EVENTS` | int | `200` | Rolling buffer size for aural events |
| `AIEAR_MEMORY_CONTEXT_WINDOW_S` | float | `120.0` | Default context window for `context_summary()` |
| `AIEAR_MEMORY_MAX_EVENTS` | int | `1000` | Rolling buffer size for aural events |
| `AIEAR_MEMORY_CONTEXT_WINDOW_S` | float | `60.0` | Default context window for `context_summary()` |
| `AIEAR_API_HOST` | string | `"0.0.0.0"` | API server listen host |
| `AIEAR_API_PORT` | int | `8080` | API server listen port |
| `AIEAR_API_WORKERS` | int | `1` | Number of uvicorn worker processes |
Expand Down
115 changes: 59 additions & 56 deletions examples/enterprise_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@
python examples/enterprise_integration.py llm-prompt
"""

from __future__ import annotations

import argparse
import asyncio
import json
import sys
import time

import numpy as np
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("ai-ear.enterprise")


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -69,36 +71,32 @@ async def analyse(self, chunk: AudioChunk) -> SpeechResult:
confidence=0.95 if detected else 0.0,
)

print("\n🔌 Enterprise Integration Demo: Custom Analyser (BYOM)\n")

keyword_spotter = KeywordSpotter(keywords=["hey ai", "alert", "emergency"])
memory = AuralMemory()
pipeline = AudioPipeline(analyzers=[keyword_spotter], memory=memory)

keywords_detected: list[str] = []

async def on_result(result):
if result.speech and result.speech.text:
keywords_detected.append(result.speech.text)
print(f" 🔑 Keyword detected: '{result.speech.text}'")

pipeline.on_result(on_result)
await pipeline.start()

SR = 16_000
for i in range(6):
chunk = AudioChunk(
samples=np.zeros(SR * 2, dtype=np.float32),
sample_rate=SR,
source_id="enterprise_mic",
)
await pipeline.process(chunk)
await asyncio.sleep(0.01)

await pipeline.stop()

print(f"\n Total keywords detected: {len(keywords_detected)}")
print(f" Keywords: {keywords_detected}\n")
try:
logger.info("🔌 Enterprise Integration Demo: Custom Analyser (BYOM)")
keyword_spotter = KeywordSpotter(keywords=["hey ai", "alert", "emergency"])
memory = AuralMemory()
pipeline = AudioPipeline(analyzers=[keyword_spotter], memory=memory)
keywords_detected: list[str] = []
async def on_result(result):
if result.speech and result.speech.text:
keywords_detected.append(result.speech.text)
logger.info(f" 🔑 Keyword detected: '{result.speech.text}'")
pipeline.on_result(on_result)
await pipeline.start()
SR = 16_000
for i in range(6):
chunk = AudioChunk(
samples=np.zeros(SR * 2, dtype=np.float32),
sample_rate=SR,
source_id="enterprise_mic",
)
await pipeline.process(chunk)
await asyncio.sleep(0.01)
await pipeline.stop()
logger.info(f" Total keywords detected: {len(keywords_detected)}")
logger.info(f" Keywords: {keywords_detected}")
except Exception as e:
logger.error(f"Custom analyser demo failed: {e}")


# ---------------------------------------------------------------------------
Expand All @@ -115,7 +113,8 @@ async def demo_alerting() -> None:
from ai_ear.core.pipeline import AudioPipeline
from ai_ear.utils.audio import generate_tone

print("\n🚨 Enterprise Integration Demo: Real-Time Alerting\n")
try:
logger.info("🚨 Enterprise Integration Demo: Real-Time Alerting")

class AlertSystem:
"""Simulates routing events to PagerDuty / Slack / SIEM etc."""
Expand All @@ -133,9 +132,9 @@ async def handle_event(self, event) -> None:
"routing": "P1_PAGERDUTY" if event.severity >= 0.8 else "SLACK_CHANNEL",
}
self.alerts.append(alert)
print(f" 🚨 [{alert['routing']}] {alert['description']} (severity={event.severity:.1f})")
logger.warning(f" 🚨 [{alert['routing']}] {alert['description']} (severity={event.severity:.1f})")
else:
print(f" ℹ️ [{event.event_type.value}] {event.description}")
logger.info(f" ℹ️ [{event.event_type.value}] {event.description}")

alerts = AlertSystem()
memory = AuralMemory()
Expand All @@ -159,8 +158,10 @@ async def handle_event(self, event) -> None:
await pipeline._process_and_dispatch(chunk)
await asyncio.sleep(0.05)

await pipeline.stop()
print(f"\n Total high-severity alerts routed: {len(alerts.alerts)}\n")
await pipeline.stop()
logger.info(f" Total high-severity alerts routed: {len(alerts.alerts)}")
except Exception as e:
logger.error(f"Alerting demo failed: {e}")


# ---------------------------------------------------------------------------
Expand All @@ -176,7 +177,8 @@ async def demo_llm_prompt() -> None:
from ai_ear.core.models import AudioChunk, EmotionLabel, EmotionProfile, SpeechSegment
from ai_ear.core.pipeline import AudioPipeline

print("\n🤖 Enterprise Integration Demo: LLM Context Injection\n")
try:
logger.info("🤖 Enterprise Integration Demo: LLM Context Injection")

memory = AuralMemory(context_window_s=120)
pipeline = AudioPipeline(
Expand Down Expand Up @@ -206,12 +208,13 @@ async def demo_llm_prompt() -> None:
summary = memory.context_summary()
system_prompt = _build_system_prompt(summary)

print(" Generated LLM System Prompt:")
print(" " + "─" * 60)
for line in system_prompt.split("\n"):
print(f" {line}")
print(" " + "─" * 60)
print()
logger.info(" Generated LLM System Prompt:")
logger.info(" " + "─" * 60)
for line in system_prompt.split("\n"):
logger.info(f" {line}")
logger.info(" " + "─" * 60)
except Exception as e:
logger.error(f"LLM prompt demo failed: {e}")


def _build_system_prompt(summary: dict) -> str:
Expand Down Expand Up @@ -259,17 +262,16 @@ def demo_serve() -> None:
from ai_ear.api.server import create_app
from ai_ear.core.config import Settings

print("\n🌐 Starting AI Ear API Server on http://0.0.0.0:8080\n")
print(" Endpoints:")
print(" GET /health — liveness probe")
print(" GET /info — configuration summary")
print(" POST /analyse — analyse an uploaded audio file")
print(" GET /memory/context — aural context summary")
print(" GET /memory/transcript — recent speech transcript")
print(" GET /memory/events — recent aural events")
print(" WS /stream — real-time WebSocket audio stream")
print(" GET /pipeline/stats — pipeline throughput stats")
print()
logger.info("🌐 Starting AI Ear API Server on http://0.0.0.0:8080")
logger.info(" Endpoints:")
logger.info(" GET /health — liveness probe")
logger.info(" GET /info — configuration summary")
logger.info(" POST /analyse — analyse an uploaded audio file")
logger.info(" GET /memory/context — aural context summary")
logger.info(" GET /memory/transcript — recent speech transcript")
logger.info(" GET /memory/events — recent aural events")
logger.info(" WS /stream — real-time WebSocket audio stream")
logger.info(" GET /pipeline/stats — pipeline throughput stats")

settings = Settings()
app = create_app(settings)
Expand All @@ -283,7 +285,8 @@ def demo_serve() -> None:
def main() -> int:
parser = argparse.ArgumentParser(description="AI Ear enterprise integration examples")
sub = parser.add_subparsers(dest="command")
sub.add_parser("custom-analyser", help="BYOM custom analyser demo")
custom = sub.add_parser("custom-analyser", help="BYOM custom analyser demo")
custom.add_argument("--keywords", nargs="+", default=["hey ai", "alert", "emergency"], help="Keywords for spotter")
sub.add_parser("alerting", help="Real-time alerting demo")
sub.add_parser("llm-prompt", help="LLM context injection demo")
sub.add_parser("serve", help="Start the API server")
Expand Down