Compare commits
1 Commits
cb/telestr
...
docstrings
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b168c53e44 |
32
.github/workflows/lint.yaml
vendored
@@ -1,32 +0,0 @@
|
||||
name: lint
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
|
||||
concurrency:
|
||||
group: build-lint-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
autopep8:
|
||||
name: "Formatting lints"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
- name: autopep8
|
||||
id: autopep8
|
||||
uses: peter-evans/autopep8@v2
|
||||
with:
|
||||
args: --exit-code -r -d -a -a src/
|
||||
- name: Fail if autopep8 requires changes
|
||||
if: steps.autopep8.outputs.exit-code == 2
|
||||
run: exit 1
|
||||
196
README.md
@@ -1,78 +1,21 @@
|
||||
# dailyai — an open source framework for real-time, multi-modal, conversational AI applications
|
||||
# Daily AI SDK
|
||||
|
||||
Build things like this:
|
||||
Build conversational, multi-modal AI apps with real-time voice and video, like this:
|
||||
|
||||
[](https://www.youtube.com/watch?v=lDevgsp9vn0)
|
||||
_Demo Video to come_
|
||||
|
||||
With built-in support for many of the best AI platforms (or [add your own](/docs)):
|
||||
|
||||
- Azure - DALL-E, ChatGPT, and Azure AI Text-to-Speech
|
||||
- Deepgram - Speech-to-text, and Aura text-to-speech
|
||||
- Eleven Labs text-to-speech
|
||||
- Fal.ai image generation
|
||||
- OpenAI DALL-E and ChatGPT
|
||||
- Whisper local speech-to-text
|
||||
|
||||
## Step 1: Get Started
|
||||
|
||||
**`dailyai` started as a toolkit for implementing generative AI voice bots.** Things like personal coaches, meeting assistants, story-telling toys for kids, customer support bots, and snarky social companions.
|
||||
|
||||
|
||||
In 2023 a *lot* of us got excited about the possibility of having open-ended conversations with LLMs. It became clear pretty quickly that we were all solving the same [low-level problems](https://www.daily.co/blog/how-to-talk-to-an-llm-with-your-voice/):
|
||||
- low-latency, reliable audio transport
|
||||
- echo cancellation
|
||||
- phrase endpointing (knowing when the bot should respond to human speech)
|
||||
- interruptibility
|
||||
- writing clean code to stream data through "pipelines" of speech-to-text, LLM inference, and text-to-speech models
|
||||
|
||||
As our applications expanded to include additional things like image generation, function calling, and vision models, we started to think about what a complete framework for these kinds of apps could look like.
|
||||
|
||||
Today, `dailyai` is:
|
||||
|
||||
1. a set of code building blocks for interacting with generative AI services and creating low-latency, interruptible data pipelines that use multiple services
|
||||
2. transport services that moves audio, video, and events across the Internet
|
||||
3. implementations of specific generative AI services
|
||||
|
||||
Currently implemented services:
|
||||
- Speech-to-text
|
||||
- Deepgram
|
||||
- Whisper
|
||||
- LLMs
|
||||
- Azure
|
||||
- OpenAI
|
||||
- Image generation
|
||||
- Azure
|
||||
- Fal
|
||||
- OpenAI
|
||||
- Text-to-speech
|
||||
- Azure
|
||||
- Deepgram
|
||||
- ElevenLabs
|
||||
- Transport
|
||||
- Daily
|
||||
- Local (in progress, intended as a quick start example service)
|
||||
|
||||
If you'd like to [implement a service]((https://github.com/daily-co/daily-ai-sdk/tree/main/src/dailyai/services)), we welcome PRs! Our goal is to support lots of services in all of the above categories, plus new categories (like real-time video) as they emerge.
|
||||
|
||||
## Step 1: Get started
|
||||
|
||||
Today, the easiest way to get started with `dailyai` is to use [Daily](https://www.daily.co/) as your transport service. This toolkit started life as an internal SDK at Daily and millions of minutes of AI conversation have been served using it and its earlier prototype incarnations. (The [transport base class](https://github.com/daily-co/daily-ai-sdk/blob/main/src/dailyai/services/base_transport_service.py) is easy to extend, though, so feel free to submit PRs if you'd like to implement another transport service.)
|
||||
|
||||
```
|
||||
# install the module
|
||||
pip install dailyai
|
||||
|
||||
# set up an .env file with API keys
|
||||
cp dot-env.template .env
|
||||
|
||||
# sign up for a free Daily account, if you don't already have one, and
|
||||
# join the Daily room URL directly from a browser tab, then run one of the
|
||||
# samples
|
||||
python src/examples/foundational/02-llm-say-one-thing.py
|
||||
```
|
||||
|
||||
## Code examples
|
||||
|
||||
There are two directories of examples:
|
||||
|
||||
- [foundational](https://github.com/daily-co/daily-ai-sdk/tree/main/src/examples/foundational) — demos that build on each other, introducing one or two concepts at a time
|
||||
- [starter apps](https://github.com/daily-co/daily-ai-sdk/tree/main/src/examples/starter-apps) — complete applications that you can use as starting points for development
|
||||
|
||||
|
||||
|
||||
## Hacking on the framework itself
|
||||
## Build/Install
|
||||
|
||||
_Note that you may need to set up a virtual environment before following the instructions below. For instance, you might need to run the following from the root of the repo:_
|
||||
|
||||
@@ -99,3 +42,118 @@ If you want to use this package from another directory, you can run:
|
||||
```
|
||||
pip install path_to_this_repo
|
||||
```
|
||||
|
||||
## Running the samples
|
||||
|
||||
Tou can run the simple sample like so:
|
||||
|
||||
```
|
||||
python src/examples/theoretical-to-real/01-say-one-thing.py -u <url of your Daily meeting> -k <your Daily API Key>
|
||||
```
|
||||
## Overview
|
||||
|
||||
The Daily AI SDK allows you to build applications that can participate in WebRTC sessions and interact with AI Services. Some examples of what you can build with this:
|
||||
|
||||
- conversational bots that interact 1:1 with a user, using voice recognition and text-to-speech
|
||||
- assistant bots that aggregate transcriptions from multiple participants in a meeting and provide realtime summaries or other AI-generated output.
|
||||
- image-recognition bots
|
||||
- etc
|
||||
|
||||
## Concepts
|
||||
|
||||
### Transport Service
|
||||
|
||||
The SDK provides one “transport service”, which is a wrapper around Daily’s `daily-python` client (tk add link). You can use this service to listen for events related to a WebRTC session, such as “a participant joined the meeting”.
|
||||
The transport service also exposes a send queue, and a receive queue. You can use the send queue to send audio and video to the WebRTC session, and you can listen to the receive queue to see audio, video and transcription data from the WebRTC session.
|
||||
|
||||
### AI Services
|
||||
|
||||
The AI Service classes provide wrappers around various AI providers, and allow you to query LLMs, convert text to speech and make images from text. The audio and images can then be placed on the transport service’s send queue, where they’ll be sent to the WebRTC session.
|
||||
|
||||
### Queue Frames
|
||||
|
||||
Communication between the transport service and AI services, and between various AI services, takes place in Queue Frames. These frames contain an indication of the type of data as well as the data itself.
|
||||
|
||||
## Using Transports, AI Services and Frames
|
||||
|
||||
AI Services all define a `.run` method. This method consumes and generates `QueueFrame` frames. The kind of frames that can be consumed and generated depend on the kind of service. For instance, an LLM AI Service consumes `LLM_MESSAGE` frames (which define a history of interaction with an LLM) and emit `TEXT` frames (the response from the LLM).
|
||||
|
||||
The `.run` method is an `AsyncIterable`, and it takes an `iterable`, `AsyncIterable` or `asyncio.Queue` that produces QueueFrames as a parameter. This makes it easy to chain AI Services, and consume input from the Transport’s `receive_queue` .
|
||||
|
||||
AI Services also have a `.run_to_queue` method. This method is not an AsyncIterable, but instead sends processed QueueFrames to a queue. This makes it easy to send the output of an AI Service to the Transport’s `send_queue`.
|
||||
|
||||
AI Services also define convenience functions that let you bypass creating QueueFrames for some simple cases (eg. using the TTS service to convert a string to audio output and send that audio to the transport’s `send_queue`). See below for examples.
|
||||
|
||||
## Examples
|
||||
|
||||
### Say Something
|
||||
|
||||
The base TTS AI service exposes a `.say` method. After creating a transport and TTS service, you can use this method like so:
|
||||
|
||||
```
|
||||
transport = DailyTransportService(...)
|
||||
tts = AzureTTSService()
|
||||
await tts.say("hello world", transport.send_queue)
|
||||
```
|
||||
|
||||
This will call the TTS service to render the text to audio frames, then put the audio frames on the transport’s send queue. The transport will then send those frames along to the WebRTC session.
|
||||
|
||||
### Speak an LLM response
|
||||
|
||||
Given a system prompt contained in a `messages` array, you can emit the LLM’s response as audio with a chain like this:
|
||||
|
||||
```
|
||||
transport = DailyTransportService(...) # setup parameters omitted
|
||||
tts = AzureTTSService()
|
||||
llm = AzureLLMService()
|
||||
messages = [...] # system prompt omitted for brevity
|
||||
|
||||
await tts.run_to_queue(
|
||||
transport.send_queue,
|
||||
llm.run([QueueFrame.LLM_MESSAGES, messages])
|
||||
)
|
||||
```
|
||||
|
||||
In this code, the LLM service object sends the messages to Azure’s OpenAI implementation, which streams chunks back asynchronously. Those chunks are aggregated by the TTS Service to ensure the best audio response (TTS works best when it gets complete sentence, so it can inflect correctly), then sent to Azure’s TTS service, converted to audio frames, and sent to the WebRTC session via the Daily transport.
|
||||
|
||||
### Pre-cache an LLM response
|
||||
|
||||
Sometimes LLMs can be slower than we’d like for natural-feeling communication. Here’s an example where we take advantage of the time it takes to speak some pre-defined text to get a head start on the LLM response:
|
||||
|
||||
(TK link to 04- sample)
|
||||
|
||||
In this sample, we set up a buffer queue to receive the audio frames from the LLM response before while we are joining the call and start an asynchronous task to start filling this buffer:
|
||||
|
||||
```
|
||||
buffer_queue = asyncio.Queue()
|
||||
llm_response_task = asyncio.create_task(
|
||||
elevenlabs_tts.run_to_queue(
|
||||
buffer_queue,
|
||||
llm.run([QueueFrame(FrameType.LLM_MESSAGE, messages)]),
|
||||
True,
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
Then, when we’ve joined the call, we speak the static text:
|
||||
|
||||
```
|
||||
await azure_tts.say("My friend...", transport.send_queue)
|
||||
```
|
||||
|
||||
As that text is being spoken, the asynchronous LLM task continues in the background. When the text is done, we pull the frames off the buffer queue and put them in the transport’s `send_queue`:
|
||||
|
||||
```
|
||||
async def buffer_to_send_queue():
|
||||
while True:
|
||||
frame = await buffer_queue.get()
|
||||
await transport.send_queue.put(frame)
|
||||
buffer_queue.task_done()
|
||||
if frame.frame_type == FrameType.END_STREAM:
|
||||
break
|
||||
|
||||
await asyncio.gather(llm_response_task, buffer_to_send_queue())
|
||||
|
||||
```
|
||||
|
||||
One thing to note here is the last parameter to `run_to_queue` in the first code clause above: this causes the `run_to_queue` method to send an `END_STREAM` frame when it’s done rendering. This lets us know when to stop our `buffer_to_send_queue` task above.
|
||||
|
||||
@@ -1,17 +1,2 @@
|
||||
# Daily AI SDK Architecture Guide
|
||||
|
||||
## Frames
|
||||
|
||||
Frames can represent discrete chunks of data, for instance a chunk of text, a chunk of audio, or an image. They can also be used to as control flow, for instance a frame that indicates that there is no more data available, or that a user started or stopped talking. They can also represent more complex data structures, such as a message array used for an LLM completion.
|
||||
|
||||
## FrameProcessors
|
||||
|
||||
Frame processors operate on frames. Every frame processor implements a `process_frame` method that consumes one frame and produces zero or more frames. Frame processors can do simple transforms, such as concatenating text fragments into sentences, or they can treat frames as input for an AI Service, and emit chat completions based on message arrays or transform text into audio or images.
|
||||
|
||||
## Pipelines
|
||||
|
||||
Pipelines are lists of frame processors that read from a source queue and send the processed frames to a sink queue. A very simple pipeline might chain an LLM frame processor to a text-to-speech frame processor, with a transport's send queue as its sync. Placing LLM message frames on the pipeline's source queue will cause the LLM's response to be spoken. See example #2 for an implementation of this.
|
||||
|
||||
## Transports
|
||||
|
||||
Transports provide a receive queue, which is input from "the outside world", and a sink queue, which is data that will be sent "to the outside world". The `LocalTransportService` does this with the local camera, mic, display and speaker. The `DailyTransportService` does this with a WebRTC session joined to a Daily.co room.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
OPENAI_API_KEY=...
|
||||
ELEVENLABS_API_KEY=...
|
||||
ELEVENLABS_VOICE_ID=...
|
||||
DAILY_API_KEY=...
|
||||
DAILY_SAMPLE_ROOM_URL=https://...
|
||||
@@ -3,25 +3,11 @@ requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "dailyai"
|
||||
version = "0.0.3.1"
|
||||
description = "An open source framework for real-time, multi-modal, conversational AI applications"
|
||||
license = { text = "BSD 2-Clause License" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.7"
|
||||
keywords = ["webrtc", "audio", "video", "ai"]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: BSD License",
|
||||
"Topic :: Communications :: Conferencing",
|
||||
"Topic :: Multimedia :: Sound/Audio",
|
||||
"Topic :: Multimedia :: Video",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence"
|
||||
]
|
||||
name = "daily_ai"
|
||||
version = "0.0.1"
|
||||
description = "Orchestrator for AI bots with Daily"
|
||||
dependencies = [
|
||||
"aiohttp",
|
||||
"anthropic",
|
||||
"azure-cognitiveservices-speech",
|
||||
"daily-python",
|
||||
"fal",
|
||||
@@ -38,10 +24,6 @@ dependencies = [
|
||||
"typing-extensions"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Source = "https://github.com/daily-co/daily-ai-sdk"
|
||||
Website = "https://daily.co"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
# All the following settings are optional:
|
||||
where = ["src"]
|
||||
|
||||
@@ -5,7 +5,6 @@ from dailyai.pipeline.frame_processor import FrameProcessor
|
||||
|
||||
from dailyai.pipeline.frames import (
|
||||
EndFrame,
|
||||
AudioFrame,
|
||||
EndPipeFrame,
|
||||
Frame,
|
||||
ImageFrame,
|
||||
@@ -15,28 +14,15 @@ from dailyai.pipeline.frames import (
|
||||
TextFrame,
|
||||
TranscriptionQueueFrame,
|
||||
UserStartedSpeakingFrame,
|
||||
UserStoppedSpeakingFrame,
|
||||
UserStoppedSpeakingFrame
|
||||
)
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.services.ai_services import AIService
|
||||
|
||||
from typing import AsyncGenerator, Callable, Coroutine, List
|
||||
|
||||
from dailyai.services.openai_llm_context import OpenAILLMContext
|
||||
|
||||
from typing import AsyncGenerator, Coroutine, List
|
||||
|
||||
class ResponseAggregator(FrameProcessor):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
messages: list[dict] | None,
|
||||
role: str,
|
||||
start_frame,
|
||||
end_frame,
|
||||
accumulator_frame,
|
||||
pass_through=True,
|
||||
):
|
||||
def __init__(self, *, messages: list[dict], role: str, start_frame, end_frame, accumulator_frame, pass_through=True):
|
||||
self.aggregation = ""
|
||||
self.aggregating = False
|
||||
self.messages = messages
|
||||
@@ -46,22 +32,16 @@ class ResponseAggregator(FrameProcessor):
|
||||
self._accumulator_frame = accumulator_frame
|
||||
self._pass_through = pass_through
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if not self.messages:
|
||||
return
|
||||
|
||||
async def process_frame(
|
||||
self, frame: Frame
|
||||
) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, self._start_frame):
|
||||
self.aggregating = True
|
||||
elif isinstance(frame, self._end_frame):
|
||||
self.aggregating = False
|
||||
# Sometimes VAD triggers quickly on and off. If we don't get any transcription,
|
||||
# it creates empty LLM message queue frames
|
||||
if len(self.aggregation) > 0:
|
||||
self.messages.append(
|
||||
{"role": self._role, "content": self.aggregation})
|
||||
self.aggregation = ""
|
||||
yield self._end_frame()
|
||||
yield LLMMessagesQueueFrame(self.messages)
|
||||
self.messages.append({"role": self._role, "content": self.aggregation})
|
||||
self.aggregation = ""
|
||||
yield LLMMessagesQueueFrame(self.messages)
|
||||
elif isinstance(frame, self._accumulator_frame) and self.aggregating:
|
||||
self.aggregation += f" {frame.text}"
|
||||
if self._pass_through:
|
||||
@@ -69,7 +49,6 @@ class ResponseAggregator(FrameProcessor):
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class LLMResponseAggregator(ResponseAggregator):
|
||||
def __init__(self, messages: list[dict]):
|
||||
super().__init__(
|
||||
@@ -77,10 +56,9 @@ class LLMResponseAggregator(ResponseAggregator):
|
||||
role="assistant",
|
||||
start_frame=LLMResponseStartFrame,
|
||||
end_frame=LLMResponseEndFrame,
|
||||
accumulator_frame=TextFrame,
|
||||
accumulator_frame=TextFrame
|
||||
)
|
||||
|
||||
|
||||
class UserResponseAggregator(ResponseAggregator):
|
||||
def __init__(self, messages: list[dict]):
|
||||
super().__init__(
|
||||
@@ -89,10 +67,9 @@ class UserResponseAggregator(ResponseAggregator):
|
||||
start_frame=UserStartedSpeakingFrame,
|
||||
end_frame=UserStoppedSpeakingFrame,
|
||||
accumulator_frame=TranscriptionQueueFrame,
|
||||
pass_through=False,
|
||||
pass_through=False
|
||||
)
|
||||
|
||||
|
||||
class LLMContextAggregator(AIService):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -110,9 +87,10 @@ class LLMContextAggregator(AIService):
|
||||
self.complete_sentences = complete_sentences
|
||||
self.pass_through = pass_through
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
# We don't do anything with non-text frames, pass it along to next in
|
||||
# the pipeline.
|
||||
async def process_frame(
|
||||
self, frame: Frame
|
||||
) -> AsyncGenerator[Frame, None]:
|
||||
# We don't do anything with non-text frames, pass it along to next in the pipeline.
|
||||
if not isinstance(frame, TextFrame):
|
||||
yield frame
|
||||
return
|
||||
@@ -134,8 +112,7 @@ class LLMContextAggregator(AIService):
|
||||
# though we check it above
|
||||
self.sentence += frame.text
|
||||
if self.sentence.endswith((".", "?", "!")):
|
||||
self.messages.append(
|
||||
{"role": self.role, "content": self.sentence})
|
||||
self.messages.append({"role": self.role, "content": self.sentence})
|
||||
self.sentence = ""
|
||||
yield LLMMessagesQueueFrame(self.messages)
|
||||
else:
|
||||
@@ -144,27 +121,19 @@ class LLMContextAggregator(AIService):
|
||||
self.messages.append({"role": self.role, "content": frame.text})
|
||||
yield LLMMessagesQueueFrame(self.messages)
|
||||
|
||||
|
||||
class LLMUserContextAggregator(LLMContextAggregator):
|
||||
def __init__(
|
||||
self,
|
||||
messages: list[dict],
|
||||
bot_participant_id=None,
|
||||
complete_sentences=True):
|
||||
self, messages: list[dict], bot_participant_id=None, complete_sentences=True
|
||||
):
|
||||
super().__init__(
|
||||
messages,
|
||||
"user",
|
||||
bot_participant_id,
|
||||
complete_sentences,
|
||||
pass_through=False)
|
||||
messages, "user", bot_participant_id, complete_sentences, pass_through=False
|
||||
)
|
||||
|
||||
|
||||
class LLMAssistantContextAggregator(LLMContextAggregator):
|
||||
def __init__(
|
||||
self,
|
||||
messages: list[dict],
|
||||
bot_participant_id=None,
|
||||
complete_sentences=True):
|
||||
self, messages: list[dict], bot_participant_id=None, complete_sentences=True
|
||||
):
|
||||
super().__init__(
|
||||
messages,
|
||||
"assistant",
|
||||
@@ -191,11 +160,12 @@ class SentenceAggregator(FrameProcessor):
|
||||
>>> asyncio.run(print_frames(aggregator, TextFrame(" world.")))
|
||||
Hello, world.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.aggregation = ""
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
async def process_frame(
|
||||
self, frame: Frame
|
||||
) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, TextFrame):
|
||||
m = re.search("(.*[?.!])(.*)", frame.text)
|
||||
if m:
|
||||
@@ -247,20 +217,15 @@ class LLMFullResponseAggregator(FrameProcessor):
|
||||
Hello, world. I am an LLM.
|
||||
LLMResponseEndFrame
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.aggregation = ""
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if not isinstance(frame, AudioFrame):
|
||||
print(f"^^^ LFRA got frame: {frame}")
|
||||
async def process_frame(
|
||||
self, frame: Frame
|
||||
) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, TextFrame):
|
||||
self.aggregation += frame.text
|
||||
print(
|
||||
f"^^^ LFRA got textframe. aggregation is now {self.aggregation}")
|
||||
elif isinstance(frame, LLMResponseEndFrame):
|
||||
print(
|
||||
f"^^^ LFRA got an llmresponseendframe. About to yield aggregation: {self.aggregation}")
|
||||
yield TextFrame(self.aggregation)
|
||||
yield frame
|
||||
self.aggregation = ""
|
||||
@@ -293,9 +258,8 @@ class StatelessTextTransformer(FrameProcessor):
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class ParallelPipeline(FrameProcessor):
|
||||
"""Run multiple pipelines in parallel.
|
||||
""" Run multiple pipelines in parallel.
|
||||
|
||||
This class takes frames from its source queue and sends them to each
|
||||
sub-pipeline. Each sub-pipeline emits its frames into this class's
|
||||
@@ -312,7 +276,6 @@ class ParallelPipeline(FrameProcessor):
|
||||
Since frame handlers pass through unhandled frames by convention, this
|
||||
class de-dupes frames in its sink before yielding them.
|
||||
"""
|
||||
|
||||
def __init__(self, pipeline_definitions: List[List[FrameProcessor]]):
|
||||
self.sources = [asyncio.Queue() for _ in pipeline_definitions]
|
||||
self.sink: asyncio.Queue[Frame] = asyncio.Queue()
|
||||
@@ -344,12 +307,10 @@ class ParallelPipeline(FrameProcessor):
|
||||
continue
|
||||
seen_ids.add(id(frame))
|
||||
|
||||
# Skip passing along EndParallelPipeQueueFrame, because we use them
|
||||
# for our own flow control.
|
||||
# Skip passing along EndParallelPipeQueueFrame, because we use them for our own flow control.
|
||||
if not isinstance(frame, EndPipeFrame):
|
||||
yield frame
|
||||
|
||||
|
||||
class GatedAggregator(FrameProcessor):
|
||||
"""Accumulate frames, with custom functions to start and stop accumulation.
|
||||
Yields gate-opening frame before any accumulated frames, then ensuing frames
|
||||
@@ -375,7 +336,6 @@ class GatedAggregator(FrameProcessor):
|
||||
>>> asyncio.run(print_frames(aggregator, TextFrame("Goodbye.")))
|
||||
Goodbye.
|
||||
"""
|
||||
|
||||
def __init__(self, gate_open_fn, gate_close_fn, start_open):
|
||||
self.gate_open_fn = gate_open_fn
|
||||
self.gate_close_fn = gate_close_fn
|
||||
|
||||
@@ -1,211 +1,79 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List
|
||||
|
||||
from dailyai.services.openai_llm_context import OpenAILLMContext
|
||||
from typing import Any
|
||||
|
||||
|
||||
class Frame:
|
||||
def __str__(self):
|
||||
return f"{self.__class__.__name__}"
|
||||
|
||||
pass
|
||||
|
||||
class ControlFrame(Frame):
|
||||
# Control frames should contain no instance data, so
|
||||
# equality is based solely on the class.
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__)
|
||||
return type(other) == self.__class__
|
||||
|
||||
|
||||
class StartFrame(ControlFrame):
|
||||
"""Used (but not required) to start a pipeline, and is also used to
|
||||
indicate that an interruption has ended and the transport should start
|
||||
processing frames again."""
|
||||
pass
|
||||
|
||||
|
||||
class EndFrame(ControlFrame):
|
||||
"""Indicates that a pipeline has ended and frame processors and pipelines
|
||||
should be shut down. If the transport receives this frame, it will stop
|
||||
sending frames to its output channel(s) and close all its threads."""
|
||||
pass
|
||||
|
||||
|
||||
class EndPipeFrame(ControlFrame):
|
||||
"""Indicates that a pipeline has ended but that the transport should
|
||||
continue processing. This frame is used in parallel pipelines and other
|
||||
sub-pipelines."""
|
||||
pass
|
||||
|
||||
|
||||
class PipelineStartedFrame(ControlFrame):
|
||||
"""
|
||||
Used by the transport to indicate that execution of a pipeline is starting
|
||||
(or restarting). It should be the first frame your app receives when it
|
||||
starts, or when an interruptible pipeline has been interrupted.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class LLMResponseStartFrame(ControlFrame):
|
||||
"""Used to indicate the beginning of an LLM response. Following TextFrames
|
||||
are part of the LLM response until an LLMResponseEndFrame"""
|
||||
pass
|
||||
|
||||
|
||||
class LLMResponseEndFrame(ControlFrame):
|
||||
"""Indicates the end of an LLM response."""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass()
|
||||
class AudioFrame(Frame):
|
||||
"""A chunk of audio. Will be played by the transport if the transport's mic
|
||||
has been enabled."""
|
||||
data: bytes
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.__class__.__name__}, size: {len(self.data)} B"
|
||||
|
||||
|
||||
@dataclass()
|
||||
class ImageFrame(Frame):
|
||||
"""An image. Will be shown by the transport if the transport's camera is
|
||||
enabled."""
|
||||
url: str | None
|
||||
image: bytes
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.__class__.__name__}, url: {self.url}, image size: {len(self.image)} B"
|
||||
|
||||
|
||||
@dataclass()
|
||||
class SpriteFrame(Frame):
|
||||
"""An animated sprite. Will be shown by the transport if the transport's
|
||||
camera is enabled. Will play at the framerate specified in the transport's
|
||||
`fps` constructor parameter."""
|
||||
images: list[bytes]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.__class__.__name__}, list size: {len(self.images)}"
|
||||
|
||||
|
||||
@dataclass()
|
||||
class TextFrame(Frame):
|
||||
"""A chunk of text. Emitted by LLM services, consumed by TTS services, can
|
||||
be used to send text through pipelines."""
|
||||
text: str
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.__class__.__name__}: "{self.text}"'
|
||||
|
||||
|
||||
@dataclass()
|
||||
class TranscriptionQueueFrame(TextFrame):
|
||||
"""A text frame with transcription-specific data. Will be placed in the
|
||||
transport's receive queue when a participant speaks."""
|
||||
participantId: str
|
||||
timestamp: str
|
||||
|
||||
|
||||
@dataclass()
|
||||
class LLMMessagesQueueFrame(Frame):
|
||||
"""A frame containing a list of LLM messages. Used to signal that an LLM
|
||||
service should run a chat completion and emit an LLMStartFrames, TextFrames
|
||||
and an LLMEndFrame.
|
||||
Note that the messages property on this class is mutable, and will be
|
||||
be updated by various ResponseAggregator frame processors."""
|
||||
messages: List[dict]
|
||||
messages: list[dict[str, str]] # TODO: define this more concretely!
|
||||
|
||||
|
||||
@dataclass()
|
||||
class OpenAILLMContextFrame(Frame):
|
||||
"""Like an LLMMessagesQueueFrame, but with extra context specific to the
|
||||
OpenAI API. The context in this message is also mutable, and will be
|
||||
changed by the OpenAIContextAggregator frame processor."""
|
||||
context: OpenAILLMContext
|
||||
|
||||
|
||||
@dataclass()
|
||||
class ReceivedAppMessageFrame(Frame):
|
||||
class AppMessageQueueFrame(Frame):
|
||||
message: Any
|
||||
sender: str
|
||||
|
||||
def __str__(self):
|
||||
return f"ReceivedAppMessageFrame: sender: {self.sender}, message: {self.message}"
|
||||
|
||||
|
||||
@dataclass()
|
||||
class SendAppMessageFrame(Frame):
|
||||
message: Any
|
||||
participantId: str | None
|
||||
|
||||
def __str__(self):
|
||||
return f"SendAppMessageFrame: participantId: {self.participantId}, message: {self.message}"
|
||||
|
||||
participantId: str
|
||||
|
||||
class UserStartedSpeakingFrame(Frame):
|
||||
"""Emitted by VAD to indicate that a participant has started speaking.
|
||||
This can be used for interruptions or other times when detecting that
|
||||
someone is speaking is more important than knowing what they're saying
|
||||
(as you will with a TranscriptionFrame)"""
|
||||
pass
|
||||
|
||||
|
||||
class UserStoppedSpeakingFrame(Frame):
|
||||
"""Emitted by the VAD to indicate that a user stopped speaking."""
|
||||
pass
|
||||
|
||||
|
||||
class BotStartedSpeakingFrame(Frame):
|
||||
pass
|
||||
|
||||
|
||||
class BotStoppedSpeakingFrame(Frame):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass()
|
||||
class LLMFunctionStartFrame(Frame):
|
||||
"""Emitted when the LLM receives the beginning of a function call
|
||||
completion. A frame processor can use this frame to indicate that it should
|
||||
start preparing to make a function call, if it can do so in the absence of
|
||||
any arguments."""
|
||||
function_name: str
|
||||
|
||||
|
||||
@dataclass()
|
||||
class LLMFunctionCallFrame(Frame):
|
||||
"""Emitted when the LLM has received an entire function call completion."""
|
||||
function_name: str
|
||||
arguments: str
|
||||
|
||||
|
||||
@dataclass()
|
||||
class VideoImageFrame(Frame):
|
||||
"""Contains a still image from a partcipant's video stream."""
|
||||
participantId: str
|
||||
image: bytes
|
||||
|
||||
# def __str__(self):
|
||||
# return f"{self.__class__.__name__}, participantId: {self.participantId}, image size: {len(self.image)} B"
|
||||
|
||||
|
||||
class TelestratorImageFrame(ImageFrame):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass()
|
||||
class VisionFrame(Frame):
|
||||
prompt: str
|
||||
image: bytes
|
||||
|
||||
# def __str__(self):
|
||||
# return f"{self.__class__.__name__}, prompt: {self.prompt}, image size: {len(self.image)} B"
|
||||
|
||||
|
||||
@dataclass()
|
||||
class RequestVideoImageFrame(Frame):
|
||||
"""Send to the transport to request a new video image from a specific participant. Leave participantId
|
||||
empty to request a frame from all participants."""
|
||||
participantId: str | None
|
||||
arguments: str
|
||||
@@ -1,24 +0,0 @@
|
||||
from typing import List
|
||||
from dailyai.pipeline.frames import EndFrame, EndPipeFrame
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
|
||||
|
||||
class SequentialMergePipeline(Pipeline):
|
||||
"""This class merges the sink queues from a list of pipelines. Frames from
|
||||
each pipeline's sink are merged in the order of pipelines in the list."""
|
||||
|
||||
def __init__(self, pipelines: List[Pipeline]):
|
||||
super().__init__([])
|
||||
self.pipelines = pipelines
|
||||
|
||||
async def run_pipeline(self):
|
||||
for pipeline in self.pipelines:
|
||||
while True:
|
||||
frame = await pipeline.sink.get()
|
||||
if isinstance(
|
||||
frame, EndFrame) or isinstance(
|
||||
frame, EndPipeFrame):
|
||||
break
|
||||
await self.sink.put(frame)
|
||||
|
||||
await self.sink.put(EndFrame())
|
||||
@@ -1,109 +0,0 @@
|
||||
from typing import Any, AsyncGenerator, Callable
|
||||
from dailyai.pipeline.frame_processor import FrameProcessor
|
||||
from dailyai.pipeline.frames import (
|
||||
Frame,
|
||||
LLMResponseEndFrame,
|
||||
LLMResponseStartFrame,
|
||||
OpenAILLMContextFrame,
|
||||
TextFrame,
|
||||
TranscriptionQueueFrame,
|
||||
UserStartedSpeakingFrame,
|
||||
UserStoppedSpeakingFrame,
|
||||
)
|
||||
from dailyai.services.openai_llm_context import OpenAILLMContext
|
||||
|
||||
from openai.types.chat import ChatCompletionRole
|
||||
|
||||
|
||||
class OpenAIContextAggregator(FrameProcessor):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: OpenAILLMContext,
|
||||
aggregator: Callable[[Frame, str | None], str | None],
|
||||
role: ChatCompletionRole,
|
||||
start_frame: type,
|
||||
end_frame: type,
|
||||
accumulator_frame: type,
|
||||
pass_through=True,
|
||||
):
|
||||
if not (
|
||||
issubclass(start_frame, Frame)
|
||||
and issubclass(end_frame, Frame)
|
||||
and issubclass(accumulator_frame, Frame)
|
||||
):
|
||||
raise TypeError(
|
||||
"start_frame, end_frame and accumulator_frame must be instances of Frame"
|
||||
)
|
||||
|
||||
self._context: OpenAILLMContext = context
|
||||
self._aggregator: Callable[[Frame, str | None], None] = aggregator
|
||||
self._role: ChatCompletionRole = role
|
||||
self._start_frame = start_frame
|
||||
self._end_frame = end_frame
|
||||
self._accumulator_frame = accumulator_frame
|
||||
self._pass_through = pass_through
|
||||
|
||||
self._aggregating = False
|
||||
self._aggregation = None
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, self._start_frame):
|
||||
self._aggregating = True
|
||||
elif isinstance(frame, self._end_frame):
|
||||
self._aggregating = False
|
||||
if self._aggregation:
|
||||
self._context.add_message(
|
||||
{
|
||||
"role": self._role,
|
||||
"content": self._aggregation,
|
||||
"name": self._role,
|
||||
} # type: ignore
|
||||
)
|
||||
self._aggregation = None
|
||||
yield OpenAILLMContextFrame(self._context)
|
||||
elif isinstance(frame, self._accumulator_frame) and self._aggregating:
|
||||
self._aggregation = self._aggregator(frame, self._aggregation)
|
||||
if self._pass_through:
|
||||
yield frame
|
||||
else:
|
||||
yield frame
|
||||
|
||||
def string_aggregator(
|
||||
self,
|
||||
frame: Frame,
|
||||
aggregation: str | None) -> str | None:
|
||||
if not isinstance(frame, TextFrame):
|
||||
raise TypeError(
|
||||
"Frame must be a TextFrame instance to be aggregated by a string aggregator."
|
||||
)
|
||||
if not aggregation:
|
||||
aggregation = ""
|
||||
return " ".join([aggregation, frame.text])
|
||||
|
||||
|
||||
class OpenAIUserContextAggregator(OpenAIContextAggregator):
|
||||
def __init__(self, context: OpenAILLMContext):
|
||||
super().__init__(
|
||||
context=context,
|
||||
aggregator=self.string_aggregator,
|
||||
role="user",
|
||||
start_frame=UserStartedSpeakingFrame,
|
||||
end_frame=UserStoppedSpeakingFrame,
|
||||
accumulator_frame=TranscriptionQueueFrame,
|
||||
pass_through=False,
|
||||
)
|
||||
|
||||
|
||||
class OpenAIAssistantContextAggregator(OpenAIContextAggregator):
|
||||
|
||||
def __init__(self, context: OpenAILLMContext):
|
||||
super().__init__(
|
||||
context,
|
||||
aggregator=self.string_aggregator,
|
||||
role="assistant",
|
||||
start_frame=LLMResponseStartFrame,
|
||||
end_frame=LLMResponseEndFrame,
|
||||
accumulator_frame=TextFrame,
|
||||
pass_through=True,
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
import asyncio
|
||||
from typing import AsyncGenerator, AsyncIterable, Iterable, List
|
||||
from typing import AsyncGenerator, List
|
||||
from dailyai.pipeline.frame_processor import FrameProcessor
|
||||
|
||||
from dailyai.pipeline.frames import EndPipeFrame, EndFrame, Frame
|
||||
@@ -17,54 +17,39 @@ class Pipeline:
|
||||
self,
|
||||
processors: List[FrameProcessor],
|
||||
source: asyncio.Queue | None = None,
|
||||
sink: asyncio.Queue[Frame] | None = None
|
||||
sink: asyncio.Queue[Frame] | None = None,
|
||||
):
|
||||
"""Create a new pipeline. By default we create the sink and source queues
|
||||
if they're not provided, but these can be overridden to point to other
|
||||
queues. If this pipeline is run by a transport, its sink and source queues
|
||||
will be overridden.
|
||||
""" Create a new pipeline. By default neither the source nor sink
|
||||
queues are set, so you'll need to pass them to this constructor or
|
||||
call set_source and set_sink before using the pipeline. Note that
|
||||
the transport's run_*_pipeline methods will set the source and sink
|
||||
queues on the pipeline for you.
|
||||
"""
|
||||
self.processors: List[FrameProcessor] = processors
|
||||
|
||||
self.source: asyncio.Queue[Frame] = source or asyncio.Queue()
|
||||
self.sink: asyncio.Queue[Frame] = sink or asyncio.Queue()
|
||||
self.processors = processors
|
||||
self.source: asyncio.Queue[Frame] | None = source
|
||||
self.sink: asyncio.Queue[Frame] | None = sink
|
||||
|
||||
def set_source(self, source: asyncio.Queue[Frame]):
|
||||
"""Set the source queue for this pipeline. Frames from this queue
|
||||
""" Set the source queue for this pipeline. Frames from this queue
|
||||
will be processed by each frame_processor in the pipeline, or order
|
||||
from first to last."""
|
||||
from first to last. """
|
||||
self.source = source
|
||||
|
||||
def set_sink(self, sink: asyncio.Queue[Frame]):
|
||||
"""Set the sink queue for this pipeline. After the last frame_processor
|
||||
""" Set the sink queue for this pipeline. After the last frame_processor
|
||||
has processed a frame, its output will be placed on this queue."""
|
||||
self.sink = sink
|
||||
|
||||
async def get_next_source_frame(self) -> AsyncGenerator[Frame, None]:
|
||||
"""Convenience function to get the next frame from the source queue. This
|
||||
""" Convenience function to get the next frame from the source queue. This
|
||||
lets us consistently have an AsyncGenerator yield frames, from either the
|
||||
source queue or a frame_processor."""
|
||||
|
||||
if self.source is None:
|
||||
raise ValueError("Source queue not set")
|
||||
yield await self.source.get()
|
||||
|
||||
async def queue_frames(
|
||||
self,
|
||||
frames: Iterable[Frame] | AsyncIterable[Frame],
|
||||
) -> None:
|
||||
"""Insert frames directly into a pipeline. This is typically used inside a transport
|
||||
participant_joined callback to prompt a bot to start a conversation, for example."""
|
||||
|
||||
if isinstance(frames, AsyncIterable):
|
||||
async for frame in frames:
|
||||
await self.source.put(frame)
|
||||
elif isinstance(frames, Iterable):
|
||||
for frame in frames:
|
||||
await self.source.put(frame)
|
||||
else:
|
||||
raise Exception("Frames must be an iterable or async iterable")
|
||||
|
||||
async def run_pipeline(self):
|
||||
"""Run the pipeline. Take each frame from the source queue, pass it to
|
||||
""" Run the pipeline. Take each frame from the source queue, pass it to
|
||||
the first frame_processor, pass the output of that frame_processor to the
|
||||
next in the list, etc. until the last frame_processor has processed the
|
||||
resulting frames, then place those frames in the sink queue.
|
||||
@@ -73,38 +58,32 @@ class Pipeline:
|
||||
|
||||
This method will exit when an EndStreamQueueFrame is placed on the sink queue.
|
||||
No more frames will be placed on the sink queue after an EndStreamQueueFrame, even
|
||||
if it's not the last frame yielded by the last frame_processor in the pipeline..
|
||||
"""
|
||||
if it's not the last frame yielded by the last frame_processor in the pipeline.."""
|
||||
|
||||
if self.source is None or self.sink is None:
|
||||
raise ValueError("Source or sink queue not set")
|
||||
|
||||
try:
|
||||
while True:
|
||||
initial_frame = await self.source.get()
|
||||
async for frame in self._run_pipeline_recursively(
|
||||
initial_frame, self.processors
|
||||
):
|
||||
await self.sink.put(frame)
|
||||
frame_generators = [self.get_next_source_frame()]
|
||||
for processor in self.processors:
|
||||
next_frame_generators = []
|
||||
for frame_generator in frame_generators:
|
||||
async for frame in frame_generator:
|
||||
next_frame_generators.append(processor.process_frame(frame))
|
||||
frame_generators = next_frame_generators
|
||||
|
||||
if isinstance(initial_frame, EndFrame) or isinstance(
|
||||
initial_frame, EndPipeFrame
|
||||
):
|
||||
break
|
||||
for frame_generator in frame_generators:
|
||||
async for frame in frame_generator:
|
||||
await self.sink.put(frame)
|
||||
if isinstance(
|
||||
frame, EndFrame
|
||||
) or isinstance(
|
||||
frame, EndPipeFrame
|
||||
):
|
||||
return
|
||||
except asyncio.CancelledError:
|
||||
# this means there's been an interruption, do any cleanup necessary
|
||||
# here.
|
||||
# this means there's been an interruption, do any cleanup necessary here.
|
||||
for processor in self.processors:
|
||||
await processor.interrupted()
|
||||
pass
|
||||
|
||||
async def _run_pipeline_recursively(
|
||||
self, initial_frame: Frame, processors: List[FrameProcessor]
|
||||
) -> AsyncGenerator[Frame, None]:
|
||||
"""Internal function to add frames to the pipeline as they're yielded
|
||||
by each processor."""
|
||||
if processors:
|
||||
async for frame in processors[0].process_frame(initial_frame):
|
||||
async for final_frame in self._run_pipeline_recursively(
|
||||
frame, processors[1:]
|
||||
):
|
||||
yield final_frame
|
||||
else:
|
||||
yield initial_frame
|
||||
|
||||
3
src/dailyai/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Pillow==10.1.0
|
||||
typing_extensions==4.9.0
|
||||
faster-whisper==0.10.0
|
||||
@@ -8,33 +8,123 @@ from dailyai.pipeline.frame_processor import FrameProcessor
|
||||
from dailyai.pipeline.frames import (
|
||||
AudioFrame,
|
||||
EndFrame,
|
||||
EndPipeFrame,
|
||||
ImageFrame,
|
||||
LLMMessagesQueueFrame,
|
||||
LLMResponseEndFrame,
|
||||
LLMResponseStartFrame,
|
||||
LLMFunctionStartFrame,
|
||||
LLMFunctionCallFrame,
|
||||
Frame,
|
||||
TextFrame,
|
||||
TranscriptionQueueFrame,
|
||||
VisionFrame
|
||||
)
|
||||
|
||||
from abc import abstractmethod
|
||||
from typing import AsyncGenerator, BinaryIO
|
||||
|
||||
from typing import AsyncGenerator, AsyncIterable, BinaryIO, Iterable, List
|
||||
|
||||
class AIService(FrameProcessor):
|
||||
""" This is the base class for various AI services (LLM, TTS and Image)
|
||||
|
||||
This class adds some convenienence functions to run, effectively, a one-stage
|
||||
pipeline where the incoming frames can come from an iterable or queue
|
||||
and the processed frames go to a queue. Child classes extend those convenience
|
||||
functions, eg. TTS's `say` method runs the TTS and emits the AudioFrames to a
|
||||
queue.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger("dailyai")
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
async def run_to_queue(
|
||||
self,
|
||||
queue: asyncio.Queue,
|
||||
frames: Iterable[Frame] | AsyncIterable[Frame] | asyncio.Queue[Frame]
|
||||
) -> None:
|
||||
""" Process the given frames (from an iterable or queue) and send them to
|
||||
the given queue.
|
||||
"""
|
||||
async for frame in self.run(frames):
|
||||
await queue.put(frame)
|
||||
|
||||
|
||||
async def run(
|
||||
self,
|
||||
frames: Iterable[Frame]
|
||||
| AsyncIterable[Frame]
|
||||
| asyncio.Queue[Frame],
|
||||
) -> AsyncGenerator[Frame, None]:
|
||||
""" Generates 0 or more frames from the given iterable or queue.
|
||||
|
||||
This is a convenience function to take a collection of frames, process
|
||||
them, and yield processed frames.
|
||||
|
||||
The preferred way to use FrameProcessors is with a pipeline, but if you
|
||||
have a very simple case (eg. a list of static text blocks you want to speak,
|
||||
or a list of static image description you want to render) this function
|
||||
will be helpful.
|
||||
"""
|
||||
try:
|
||||
if isinstance(frames, AsyncIterable):
|
||||
async for frame in frames:
|
||||
async for output_frame in self.process_frame(frame):
|
||||
yield output_frame
|
||||
elif isinstance(frames, Iterable):
|
||||
for frame in frames:
|
||||
async for output_frame in self.process_frame(frame):
|
||||
yield output_frame
|
||||
elif isinstance(frames, asyncio.Queue):
|
||||
while True:
|
||||
frame = await frames.get()
|
||||
async for output_frame in self.process_frame(frame):
|
||||
yield output_frame
|
||||
if isinstance(frame, EndFrame):
|
||||
break
|
||||
else:
|
||||
raise Exception("Frames must be an iterable or async iterable")
|
||||
except Exception as e:
|
||||
self.logger.error("Exception occurred while running AI service", e)
|
||||
raise e
|
||||
|
||||
|
||||
class LLMService(AIService):
|
||||
"""This class is a no-op but serves as a base class for LLM services."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, messages=None, tools=None):
|
||||
super().__init__()
|
||||
self._tools = tools
|
||||
self._messages = messages
|
||||
|
||||
@abstractmethod
|
||||
async def run_llm_async(self, messages, tool_choice=None) -> AsyncGenerator[str, None]:
|
||||
yield ""
|
||||
|
||||
@abstractmethod
|
||||
async def run_llm(self, messages) -> str:
|
||||
pass
|
||||
|
||||
async def process_frame(self, frame: Frame, tool_choice: str | None = None) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, LLMMessagesQueueFrame):
|
||||
function_name = ""
|
||||
arguments = ""
|
||||
if isinstance(frame, LLMMessagesQueueFrame):
|
||||
yield LLMResponseStartFrame()
|
||||
async for text_chunk in self.run_llm_async(frame.messages, tool_choice):
|
||||
if isinstance(text_chunk, str):
|
||||
yield TextFrame(text_chunk)
|
||||
elif text_chunk.function:
|
||||
if text_chunk.function.name:
|
||||
# function_name += text_chunk.function.name
|
||||
yield LLMFunctionCallFrame(function_name=text_chunk.function.name, arguments=None)
|
||||
if text_chunk.function.arguments:
|
||||
# arguments += text_chunk.function.arguments
|
||||
yield LLMFunctionCallFrame(function_name=None, arguments=text_chunk.function.arguments)
|
||||
|
||||
if (function_name and arguments):
|
||||
function_name = ""
|
||||
arguments = ""
|
||||
yield LLMResponseEndFrame()
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class TTSService(AIService):
|
||||
@@ -55,14 +145,13 @@ class TTSService(AIService):
|
||||
yield bytes()
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, EndFrame) or isinstance(frame, EndPipeFrame):
|
||||
if isinstance(frame, EndFrame):
|
||||
if self.current_sentence:
|
||||
async for audio_chunk in self.run_tts(self.current_sentence):
|
||||
yield AudioFrame(audio_chunk)
|
||||
yield TextFrame(self.current_sentence)
|
||||
|
||||
if not isinstance(frame, TextFrame):
|
||||
print(f"*** tts yielding non-text: {frame}")
|
||||
yield frame
|
||||
return
|
||||
|
||||
@@ -71,7 +160,7 @@ class TTSService(AIService):
|
||||
text = frame.text
|
||||
else:
|
||||
self.current_sentence += frame.text
|
||||
if self.current_sentence.strip().endswith((".", "?", "!")):
|
||||
if self.current_sentence.endswith((".", "?", "!")):
|
||||
text = self.current_sentence
|
||||
self.current_sentence = ""
|
||||
|
||||
@@ -79,11 +168,13 @@ class TTSService(AIService):
|
||||
async for audio_chunk in self.run_tts(text):
|
||||
yield AudioFrame(audio_chunk)
|
||||
|
||||
# note we pass along the text frame *after* the audio, so the text
|
||||
# frame is completed after the audio is processed.
|
||||
print(f"*** tts yielding text: {text}")
|
||||
# note we pass along the text frame *after* the audio, so the text frame is completed after the audio is processed.
|
||||
yield TextFrame(text)
|
||||
|
||||
# Convenience function to send the audio for a sentence to the given queue
|
||||
async def say(self, sentence, queue: asyncio.Queue):
|
||||
await self.run_to_queue(queue, [LLMResponseStartFrame(), TextFrame(sentence), LLMResponseEndFrame()])
|
||||
|
||||
|
||||
class ImageGenService(AIService):
|
||||
def __init__(self, image_size, **kwargs):
|
||||
@@ -133,28 +224,7 @@ class STTService(AIService):
|
||||
ww.close()
|
||||
content.seek(0)
|
||||
text = await self.run_stt(content)
|
||||
yield TranscriptionQueueFrame(text, "", str(time.time()))
|
||||
|
||||
|
||||
class VisionService(AIService):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# Renders the image. Returns an Image object.
|
||||
# TODO-CB: return type
|
||||
@abstractmethod
|
||||
async def run_vision(self, prompt: str, image: bytes):
|
||||
pass
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, VisionFrame):
|
||||
async for frame in self.run_vision(frame.prompt, frame.image):
|
||||
print(
|
||||
f"&&& visionservce processframe got frame to yield: {frame}")
|
||||
yield frame
|
||||
yield LLMResponseEndFrame()
|
||||
else:
|
||||
yield frame
|
||||
yield TranscriptionQueueFrame(text, '', str(time.time()))
|
||||
|
||||
|
||||
class FrameLogger(AIService):
|
||||
@@ -163,9 +233,8 @@ class FrameLogger(AIService):
|
||||
self.prefix = prefix
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, (AudioFrame)):
|
||||
# self.logger.info(f"{self.prefix}: {type(frame)}")
|
||||
pass
|
||||
if isinstance(frame, (AudioFrame, ImageFrame)):
|
||||
self.logger.info(f"{self.prefix}: {type(frame)}")
|
||||
else:
|
||||
print(f"{self.prefix}: {frame}")
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from typing import AsyncGenerator
|
||||
from anthropic import AsyncAnthropic
|
||||
from dailyai.pipeline.frames import Frame, LLMMessagesQueueFrame, TextFrame
|
||||
|
||||
from dailyai.services.ai_services import LLMService
|
||||
|
||||
|
||||
class AnthropicLLMService(LLMService):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key,
|
||||
model="claude-3-opus-20240229",
|
||||
max_tokens=1024):
|
||||
super().__init__()
|
||||
self.client = AsyncAnthropic(api_key=api_key)
|
||||
self.model = model
|
||||
self.max_tokens = max_tokens
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if not isinstance(frame, LLMMessagesQueueFrame):
|
||||
yield frame
|
||||
|
||||
stream = await self.client.messages.create(
|
||||
max_tokens=self.max_tokens,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello, Claude",
|
||||
}
|
||||
],
|
||||
model=self.model,
|
||||
stream=True,
|
||||
)
|
||||
async for event in stream:
|
||||
if event.type == "content_block_delta":
|
||||
yield TextFrame(event.delta.text)
|
||||
@@ -14,37 +14,27 @@ from dailyai.services.ai_services import LLMService, TTSService, ImageGenService
|
||||
from PIL import Image
|
||||
|
||||
# See .env.example for Azure configuration needed
|
||||
from azure.cognitiveservices.speech import (
|
||||
SpeechSynthesizer,
|
||||
SpeechConfig,
|
||||
ResultReason,
|
||||
CancellationReason,
|
||||
)
|
||||
|
||||
from dailyai.services.openai_api_llm_service import BaseOpenAILLMService
|
||||
from azure.cognitiveservices.speech import SpeechSynthesizer, SpeechConfig, ResultReason, CancellationReason
|
||||
|
||||
|
||||
class AzureTTSService(TTSService):
|
||||
def __init__(self, *, api_key, region, voice="en-US-SaraNeural"):
|
||||
def __init__(self, *, api_key, region):
|
||||
super().__init__()
|
||||
|
||||
self.speech_config = SpeechConfig(subscription=api_key, region=region)
|
||||
self.speech_synthesizer = SpeechSynthesizer(
|
||||
speech_config=self.speech_config, audio_config=None
|
||||
)
|
||||
self._voice = voice
|
||||
speech_config=self.speech_config, audio_config=None)
|
||||
|
||||
async def run_tts(self, sentence) -> AsyncGenerator[bytes, None]:
|
||||
self.logger.info("Running azure tts")
|
||||
ssml = (
|
||||
"<speak version='1.0' xml:lang='en-US' xmlns='http://www.w3.org/2001/10/synthesis' "
|
||||
"xmlns:mstts='http://www.w3.org/2001/mstts'>"
|
||||
f"<voice name='{self._voice}'>"
|
||||
"<mstts:silence type='Sentenceboundary' value='20ms' />"
|
||||
"<mstts:express-as style='lyrical' styledegree='2' role='SeniorFemale'>"
|
||||
"<prosody rate='1.05'>"
|
||||
f"{sentence}"
|
||||
"</prosody></mstts:express-as></voice></speak> ")
|
||||
ssml = "<speak version='1.0' xml:lang='en-US' xmlns='http://www.w3.org/2001/10/synthesis' " \
|
||||
"xmlns:mstts='http://www.w3.org/2001/mstts'>" \
|
||||
"<voice name='en-US-SaraNeural'>" \
|
||||
"<mstts:silence type='Sentenceboundary' value='20ms' />" \
|
||||
"<mstts:express-as style='lyrical' styledegree='2' role='SeniorFemale'>" \
|
||||
"<prosody rate='1.05'>" \
|
||||
f"{sentence}" \
|
||||
"</prosody></mstts:express-as></voice></speak> "
|
||||
result = await asyncio.to_thread(self.speech_synthesizer.speak_ssml, (ssml))
|
||||
self.logger.info("Got azure tts result")
|
||||
if result.reason == ResultReason.SynthesizingAudioCompleted:
|
||||
@@ -53,49 +43,62 @@ class AzureTTSService(TTSService):
|
||||
yield result.audio_data[44:]
|
||||
elif result.reason == ResultReason.Canceled:
|
||||
cancellation_details = result.cancellation_details
|
||||
self.logger.info(
|
||||
"Speech synthesis canceled: {}".format(
|
||||
cancellation_details.reason))
|
||||
self.logger.info("Speech synthesis canceled: {}".format(cancellation_details.reason))
|
||||
if cancellation_details.reason == CancellationReason.Error:
|
||||
self.logger.info(
|
||||
"Error details: {}".format(
|
||||
cancellation_details.error_details))
|
||||
self.logger.info("Error details: {}".format(cancellation_details.error_details))
|
||||
|
||||
|
||||
class AzureLLMService(BaseOpenAILLMService):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key,
|
||||
endpoint,
|
||||
api_version="2023-12-01-preview",
|
||||
model):
|
||||
self._endpoint = endpoint
|
||||
self._api_version = api_version
|
||||
|
||||
super().__init__(api_key=api_key, model=model)
|
||||
class AzureLLMService(LLMService):
|
||||
def __init__(self, *, api_key, endpoint, api_version="2023-12-01-preview", model, tools=None, messages=None):
|
||||
super().__init__(tools=tools, messages=messages)
|
||||
self._model: str = model
|
||||
|
||||
def create_client(self, api_key=None, base_url=None):
|
||||
self._client = AsyncAzureOpenAI(
|
||||
api_key=api_key,
|
||||
azure_endpoint=self._endpoint,
|
||||
api_version=self._api_version,
|
||||
azure_endpoint=endpoint,
|
||||
api_version=api_version,
|
||||
)
|
||||
|
||||
async def run_llm_async(self, messages, tool_choice=None) -> AsyncGenerator[str, None]:
|
||||
messages_for_log = json.dumps(messages)
|
||||
self.logger.debug(f"Generating chat via azure: {messages_for_log}")
|
||||
if self._tools:
|
||||
tools = self._tools
|
||||
else:
|
||||
tools = None
|
||||
start_time = time.time()
|
||||
chunks = await self._client.chat.completions.create(model=self._model, stream=True, messages=messages, tools=tools, tool_choice=tool_choice)
|
||||
self.logger.info(f"=== Azure OpenAI LLM TTFB: {time.time() - start_time}")
|
||||
async for chunk in chunks:
|
||||
if len(chunk.choices) == 0:
|
||||
continue
|
||||
if chunk.choices[0].delta.tool_calls:
|
||||
yield chunk.choices[0].delta.tool_calls[0]
|
||||
elif chunk.choices[0].delta.content:
|
||||
yield chunk.choices[0].delta.content
|
||||
|
||||
async def run_llm(self, messages) -> str | None:
|
||||
messages_for_log = json.dumps(messages)
|
||||
self.logger.debug(f"Generating chat via azure: {messages_for_log}")
|
||||
|
||||
response = await self._client.chat.completions.create(model=self._model, stream=False, messages=messages)
|
||||
if response and len(response.choices) > 0:
|
||||
return response.choices[0].message.content
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class AzureImageGenServiceREST(ImageGenService):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_version="2023-06-01-preview",
|
||||
image_size: str,
|
||||
aiohttp_session: aiohttp.ClientSession,
|
||||
api_key,
|
||||
endpoint,
|
||||
model,
|
||||
):
|
||||
self,
|
||||
*,
|
||||
api_version="2023-06-01-preview",
|
||||
image_size: str,
|
||||
aiohttp_session: aiohttp.ClientSession,
|
||||
api_key,
|
||||
endpoint,
|
||||
model):
|
||||
super().__init__(image_size=image_size)
|
||||
|
||||
self._api_key = api_key
|
||||
@@ -106,9 +109,7 @@ class AzureImageGenServiceREST(ImageGenService):
|
||||
|
||||
async def run_image_gen(self, sentence) -> tuple[str, bytes]:
|
||||
url = f"{self._azure_endpoint}openai/images/generations:submit?api-version={self._api_version}"
|
||||
headers = {
|
||||
"api-key": self._api_key,
|
||||
"Content-Type": "application/json"}
|
||||
headers = {"api-key": self._api_key, "Content-Type": "application/json"}
|
||||
body = {
|
||||
# Enter your prompt text here
|
||||
"prompt": sentence,
|
||||
@@ -119,9 +120,8 @@ class AzureImageGenServiceREST(ImageGenService):
|
||||
url, headers=headers, json=body
|
||||
) as submission:
|
||||
# We never get past this line, because this header isn't
|
||||
# defined on a 429 response, but something is eating our
|
||||
# exceptions!
|
||||
operation_location = submission.headers["operation-location"]
|
||||
# defined on a 429 response, but something is eating our exceptions!
|
||||
operation_location = submission.headers['operation-location']
|
||||
status = ""
|
||||
attempts_left = 120
|
||||
json_response = None
|
||||
@@ -137,8 +137,7 @@ class AzureImageGenServiceREST(ImageGenService):
|
||||
json_response = await response.json()
|
||||
status = json_response["status"]
|
||||
|
||||
image_url = (
|
||||
json_response["result"]["data"][0]["url"] if json_response else None)
|
||||
image_url = json_response["result"]["data"][0]["url"] if json_response else None
|
||||
if not image_url:
|
||||
raise Exception("Image generation failed")
|
||||
# Load the image from the url
|
||||
|
||||
@@ -8,53 +8,52 @@ import torch
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, AsyncGenerator
|
||||
from typing import AsyncGenerator
|
||||
from enum import Enum
|
||||
from dailyai.pipeline.frame_processor import FrameProcessor
|
||||
|
||||
from dailyai.pipeline.frames import (
|
||||
SendAppMessageFrame,
|
||||
AudioFrame,
|
||||
EndFrame,
|
||||
ImageFrame,
|
||||
Frame,
|
||||
PipelineStartedFrame,
|
||||
SpriteFrame,
|
||||
StartFrame,
|
||||
TextFrame,
|
||||
TranscriptionQueueFrame,
|
||||
UserStartedSpeakingFrame,
|
||||
UserStoppedSpeakingFrame,
|
||||
RequestVideoImageFrame,
|
||||
TelestratorImageFrame
|
||||
UserStoppedSpeakingFrame
|
||||
)
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.services.ai_services import TTSService
|
||||
|
||||
torch.set_num_threads(1)
|
||||
|
||||
model, utils = torch.hub.load(
|
||||
repo_or_dir="snakers4/silero-vad", model="silero_vad", force_reload=False
|
||||
)
|
||||
model, utils = torch.hub.load(repo_or_dir='snakers4/silero-vad',
|
||||
model='silero_vad',
|
||||
force_reload=False)
|
||||
|
||||
(get_speech_timestamps, save_audio, read_audio, VADIterator, collect_chunks) = utils
|
||||
(get_speech_timestamps,
|
||||
save_audio,
|
||||
read_audio,
|
||||
VADIterator,
|
||||
collect_chunks) = utils
|
||||
|
||||
# Taken from utils_vad.py
|
||||
|
||||
|
||||
def validate(model, inputs: torch.Tensor):
|
||||
def validate(model,
|
||||
inputs: torch.Tensor):
|
||||
with torch.no_grad():
|
||||
outs = model(inputs)
|
||||
return outs
|
||||
|
||||
|
||||
# Provided by Alexander Veysov
|
||||
|
||||
|
||||
def int2float(sound):
|
||||
abs_max = np.abs(sound).max()
|
||||
sound = sound.astype("float32")
|
||||
sound = sound.astype('float32')
|
||||
if abs_max > 0:
|
||||
sound *= 1 / 32768
|
||||
sound *= 1/32768
|
||||
sound = sound.squeeze() # depends on the use case
|
||||
return sound
|
||||
|
||||
@@ -74,7 +73,7 @@ class VADState(Enum):
|
||||
STOPPING = 4
|
||||
|
||||
|
||||
class BaseTransportService:
|
||||
class BaseTransportService():
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -92,13 +91,10 @@ class BaseTransportService:
|
||||
self._vad_stop_s = kwargs.get("vad_stop_s") or 0.8
|
||||
self._context = kwargs.get("context") or []
|
||||
self._vad_enabled = kwargs.get("vad_enabled") or False
|
||||
self._receive_video = kwargs.get("receive_video") or False
|
||||
self._receive_video_fps = kwargs.get("receive_video_fps") or 0.0
|
||||
self._participant_frame_times = {}
|
||||
|
||||
if self._vad_enabled and self._speaker_enabled:
|
||||
raise Exception(
|
||||
"Sorry, you can't use speaker_enabled and vad_enabled at the same time. Please set one to False."
|
||||
)
|
||||
"Sorry, you can't use speaker_enabled and vad_enabled at the same time. Please set one to False.")
|
||||
|
||||
self._vad_samples = 1536
|
||||
vad_frame_s = self._vad_samples / SAMPLE_RATE
|
||||
@@ -131,7 +127,7 @@ class BaseTransportService:
|
||||
|
||||
self._logger: logging.Logger = logging.getLogger()
|
||||
|
||||
async def run(self, pipeline: Pipeline | None = None, override_pipeline_source_queue=True):
|
||||
async def run(self):
|
||||
self._prerun()
|
||||
|
||||
async_output_queue_marshal_task = asyncio.create_task(
|
||||
@@ -142,28 +138,23 @@ class BaseTransportService:
|
||||
self._camera_thread.start()
|
||||
|
||||
self._frame_consumer_thread = threading.Thread(
|
||||
target=self._frame_consumer, daemon=True
|
||||
)
|
||||
target=self._frame_consumer, daemon=True)
|
||||
self._frame_consumer_thread.start()
|
||||
|
||||
if self._speaker_enabled:
|
||||
self._receive_audio_thread = threading.Thread(
|
||||
target=self._receive_audio, daemon=True
|
||||
)
|
||||
target=self._receive_audio, daemon=True)
|
||||
self._receive_audio_thread.start()
|
||||
|
||||
if self._vad_enabled:
|
||||
self._vad_thread = threading.Thread(target=self._vad, daemon=True)
|
||||
self._vad_thread.start()
|
||||
|
||||
pipeline_task = None
|
||||
if pipeline:
|
||||
pipeline_task = asyncio.create_task(
|
||||
self.run_pipeline(pipeline, override_pipeline_source_queue)
|
||||
)
|
||||
|
||||
try:
|
||||
while time.time() < self._expiration and not self._stop_threads.is_set():
|
||||
while (
|
||||
time.time() < self._expiration
|
||||
and not self._stop_threads.is_set()
|
||||
):
|
||||
await asyncio.sleep(1)
|
||||
except Exception as e:
|
||||
self._logger.error(f"Exception {e}")
|
||||
@@ -174,12 +165,9 @@ class BaseTransportService:
|
||||
|
||||
self._stop_threads.set()
|
||||
|
||||
if pipeline_task:
|
||||
pipeline_task.cancel()
|
||||
|
||||
await self.send_queue.put(EndFrame())
|
||||
|
||||
await async_output_queue_marshal_task
|
||||
await self.send_queue.join()
|
||||
self._frame_consumer_thread.join()
|
||||
|
||||
if self._speaker_enabled:
|
||||
@@ -188,10 +176,9 @@ class BaseTransportService:
|
||||
if self._vad_enabled:
|
||||
self._vad_thread.join()
|
||||
|
||||
async def run_pipeline(self, pipeline: Pipeline, override_pipeline_source_queue=True):
|
||||
async def run_uninterruptible_pipeline(self, pipeline: Pipeline):
|
||||
pipeline.set_sink(self.send_queue)
|
||||
if override_pipeline_source_queue:
|
||||
pipeline.set_source(self.receive_queue)
|
||||
pipeline.set_source(self.receive_queue)
|
||||
await pipeline.run_pipeline()
|
||||
|
||||
async def run_interruptible_pipeline(
|
||||
@@ -223,8 +210,7 @@ class BaseTransportService:
|
||||
break
|
||||
|
||||
if post_processor:
|
||||
post_process_task = asyncio.create_task(
|
||||
post_process(post_processor))
|
||||
post_process_task = asyncio.create_task(post_process(post_processor))
|
||||
|
||||
started = False
|
||||
|
||||
@@ -251,11 +237,6 @@ class BaseTransportService:
|
||||
|
||||
await asyncio.gather(pipeline_task, post_process_task)
|
||||
|
||||
async def say(self, text: str, tts: TTSService):
|
||||
"""Say a phrase. Use with caution; this bypasses any running pipelines."""
|
||||
async for frame in tts.process_frame(TextFrame(text)):
|
||||
await self.send_queue.put(frame)
|
||||
|
||||
def _post_run(self):
|
||||
# Note that this function must be idempotent! It can be called multiple times
|
||||
# if, for example, a keyboard interrupt occurs.
|
||||
@@ -322,25 +303,19 @@ class BaseTransportService:
|
||||
case VADState.STOPPING:
|
||||
self._vad_stopping_count += 1
|
||||
|
||||
if (
|
||||
self._vad_state == VADState.STARTING
|
||||
and self._vad_starting_count >= self._vad_start_frames
|
||||
):
|
||||
if self._loop:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.receive_queue.put(
|
||||
UserStartedSpeakingFrame()), self._loop)
|
||||
if self._vad_state == VADState.STARTING and self._vad_starting_count >= self._vad_start_frames:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.receive_queue.put(
|
||||
UserStartedSpeakingFrame()), self._loop
|
||||
)
|
||||
# self.interrupt()
|
||||
self._vad_state = VADState.SPEAKING
|
||||
self._vad_starting_count = 0
|
||||
if (
|
||||
self._vad_state == VADState.STOPPING
|
||||
and self._vad_stopping_count >= self._vad_stop_frames
|
||||
):
|
||||
if self._loop:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.receive_queue.put(
|
||||
UserStoppedSpeakingFrame()), self._loop)
|
||||
if self._vad_state == VADState.STOPPING and self._vad_stopping_count >= self._vad_stop_frames:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.receive_queue.put(
|
||||
UserStoppedSpeakingFrame()), self._loop
|
||||
)
|
||||
self._vad_state = VADState.QUIET
|
||||
self._vad_stopping_count = 0
|
||||
|
||||
@@ -353,7 +328,7 @@ class BaseTransportService:
|
||||
break
|
||||
|
||||
def interrupt(self):
|
||||
self._logger.debug("### Interrupting")
|
||||
self._logger.debug("!!! Interrupting")
|
||||
self._is_interrupted.set()
|
||||
|
||||
async def get_receive_frames(self) -> AsyncGenerator[Frame, None]:
|
||||
@@ -379,8 +354,8 @@ class BaseTransportService:
|
||||
)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.receive_queue.put(
|
||||
EndFrame()), self._loop)
|
||||
self.receive_queue.put(EndFrame()), self._loop
|
||||
)
|
||||
|
||||
def _set_image(self, image: bytes):
|
||||
self._images = itertools.cycle([image])
|
||||
@@ -388,10 +363,6 @@ class BaseTransportService:
|
||||
def _set_images(self, images: list[bytes], start_frame=0):
|
||||
self._images = itertools.cycle(images)
|
||||
|
||||
def send_app_message(self, message: Any, participantId: str | None):
|
||||
""" Child classes should override this to send a custom message to the room. """
|
||||
pass
|
||||
|
||||
def _run_camera(self):
|
||||
try:
|
||||
while not self._stop_threads.is_set():
|
||||
@@ -409,20 +380,18 @@ class BaseTransportService:
|
||||
b = bytearray()
|
||||
smallest_write_size = 3200
|
||||
largest_write_size = 8000
|
||||
all_audio_frames = bytearray()
|
||||
while True:
|
||||
try:
|
||||
frames_or_frame: Frame | list[Frame] = self._threadsafe_send_queue.get(
|
||||
frames_or_frame: Frame | list[Frame] = (
|
||||
self._threadsafe_send_queue.get()
|
||||
)
|
||||
if (
|
||||
isinstance(frames_or_frame, AudioFrame)
|
||||
and len(frames_or_frame.data) > largest_write_size
|
||||
):
|
||||
if isinstance(frames_or_frame, AudioFrame) and len(frames_or_frame.data) > largest_write_size:
|
||||
# subdivide large audio frames to enable interruption
|
||||
frames = []
|
||||
for i in range(0, len(frames_or_frame.data),
|
||||
largest_write_size):
|
||||
for i in range(0, len(frames_or_frame.data), largest_write_size):
|
||||
frames.append(AudioFrame(
|
||||
frames_or_frame.data[i: i + largest_write_size]))
|
||||
frames_or_frame.data[i: i+largest_write_size]))
|
||||
elif isinstance(frames_or_frame, Frame):
|
||||
frames: list[Frame] = [frames_or_frame]
|
||||
elif isinstance(frames_or_frame, list):
|
||||
@@ -433,7 +402,6 @@ class BaseTransportService:
|
||||
for frame in frames:
|
||||
if isinstance(frame, EndFrame):
|
||||
self._logger.info("Stopping frame consumer thread")
|
||||
self._stop_threads.set()
|
||||
self._threadsafe_send_queue.task_done()
|
||||
if self._loop:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
@@ -441,13 +409,12 @@ class BaseTransportService:
|
||||
)
|
||||
return
|
||||
|
||||
# if interrupted, we just pull frames off the queue and
|
||||
# discard them
|
||||
# if interrupted, we just pull frames off the queue and discard them
|
||||
if not self._is_interrupted.is_set():
|
||||
if frame:
|
||||
|
||||
if isinstance(frame, AudioFrame):
|
||||
chunk = frame.data
|
||||
all_audio_frames.extend(chunk)
|
||||
|
||||
b.extend(chunk)
|
||||
truncated_length: int = len(b) - (
|
||||
@@ -457,28 +424,10 @@ class BaseTransportService:
|
||||
self.write_frame_to_mic(
|
||||
bytes(b[:truncated_length]))
|
||||
b = b[truncated_length:]
|
||||
elif isinstance(frame, TelestratorImageFrame):
|
||||
self._set_image(frame.image)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.receive_queue.put(frame),
|
||||
self._loop,
|
||||
)
|
||||
elif isinstance(frame, ImageFrame):
|
||||
self._set_image(frame.image)
|
||||
elif isinstance(frame, SpriteFrame):
|
||||
self._set_images(frame.images)
|
||||
elif isinstance(frame, SendAppMessageFrame):
|
||||
self.send_app_message(
|
||||
frame.message, frame.participantId)
|
||||
elif isinstance(frame, RequestVideoImageFrame):
|
||||
# removing one or all participant IDs from _participant_frame_times
|
||||
# will cause the transport to send the next available frame from
|
||||
# that participant
|
||||
if frame.participantId:
|
||||
self._participant_frame_times.pop(
|
||||
frame.participantId, None)
|
||||
else:
|
||||
self._participant_frame_times.clear()
|
||||
elif len(b):
|
||||
self.write_frame_to_mic(bytes(b))
|
||||
b = bytearray()
|
||||
@@ -493,10 +442,6 @@ class BaseTransportService:
|
||||
|
||||
if isinstance(frame, StartFrame):
|
||||
self._is_interrupted.clear()
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.receive_queue.put(PipelineStartedFrame()),
|
||||
self._loop,
|
||||
)
|
||||
|
||||
if self._loop:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
|
||||
@@ -2,18 +2,13 @@ import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
import signal
|
||||
import time
|
||||
import threading
|
||||
import types
|
||||
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
from dailyai.pipeline.frames import (
|
||||
ReceivedAppMessageFrame,
|
||||
TranscriptionQueueFrame,
|
||||
VideoImageFrame,
|
||||
TelestratorImageFrame
|
||||
)
|
||||
|
||||
from threading import Event
|
||||
@@ -51,8 +46,7 @@ class DailyTransportService(BaseTransportService, EventHandler):
|
||||
start_transcription: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
# This will call BaseTransportService.__init__ method, not EventHandler
|
||||
super().__init__(**kwargs)
|
||||
super().__init__(**kwargs) # This will call BaseTransportService.__init__ method, not EventHandler
|
||||
|
||||
self._room_url: str = room_url
|
||||
self._bot_name: str = bot_name
|
||||
@@ -87,12 +81,7 @@ class DailyTransportService(BaseTransportService, EventHandler):
|
||||
for handler in self._event_handlers[event_name]:
|
||||
if inspect.iscoroutinefunction(handler):
|
||||
if self._loop:
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
handler(*args, **kwargs), self._loop)
|
||||
|
||||
# wait for the coroutine to finish. This will also
|
||||
# raise any exceptions raised by the coroutine.
|
||||
future.result()
|
||||
asyncio.run_coroutine_threadsafe(handler(*args, **kwargs), self._loop)
|
||||
else:
|
||||
raise Exception(
|
||||
"No event loop to run coroutine. In order to use async event handlers, you must run the DailyTransportService in an asyncio event loop.")
|
||||
@@ -104,8 +93,7 @@ class DailyTransportService(BaseTransportService, EventHandler):
|
||||
|
||||
def add_event_handler(self, event_name: str, handler):
|
||||
if not event_name.startswith("on_"):
|
||||
raise Exception(
|
||||
f"Event handler {event_name} must start with 'on_'")
|
||||
raise Exception(f"Event handler {event_name} must start with 'on_'")
|
||||
|
||||
methods = inspect.getmembers(self, predicate=inspect.ismethod)
|
||||
if event_name not in [method[0] for method in methods]:
|
||||
@@ -118,8 +106,7 @@ class DailyTransportService(BaseTransportService, EventHandler):
|
||||
handler, self)]
|
||||
setattr(self, event_name, partial(self._patch_method, event_name))
|
||||
else:
|
||||
self._event_handlers[event_name].append(
|
||||
types.MethodType(handler, self))
|
||||
self._event_handlers[event_name].append(types.MethodType(handler, self))
|
||||
|
||||
def event_handler(self, event_name: str):
|
||||
def decorator(handler):
|
||||
@@ -134,9 +121,6 @@ class DailyTransportService(BaseTransportService, EventHandler):
|
||||
def write_frame_to_mic(self, frame: bytes):
|
||||
self.mic.write_frames(frame)
|
||||
|
||||
def send_app_message(self, message: Any, participantId: str | None):
|
||||
self.client.send_app_message(message, participantId)
|
||||
|
||||
def read_audio_frames(self, desired_frame_count):
|
||||
bytes = self._speaker.read_frames(desired_frame_count)
|
||||
return bytes
|
||||
@@ -156,7 +140,8 @@ class DailyTransportService(BaseTransportService, EventHandler):
|
||||
|
||||
if self._camera_enabled:
|
||||
self.camera: VirtualCameraDevice = Daily.create_camera_device(
|
||||
"camera", width=self._camera_width, height=self._camera_height, color_format="RGB")
|
||||
"camera", width=self._camera_width, height=self._camera_height, color_format="RGB"
|
||||
)
|
||||
|
||||
if self._speaker_enabled or self._vad_enabled:
|
||||
self._speaker: VirtualSpeakerDevice = Daily.create_speaker_device(
|
||||
@@ -207,12 +192,11 @@ class DailyTransportService(BaseTransportService, EventHandler):
|
||||
)
|
||||
self._my_participant_id = self.client.participants()["local"]["id"]
|
||||
|
||||
if not self._receive_video:
|
||||
self.client.update_subscription_profiles({
|
||||
"base": {
|
||||
"camera": "unsubscribed",
|
||||
}
|
||||
})
|
||||
self.client.update_subscription_profiles({
|
||||
"base": {
|
||||
"camera": "unsubscribed",
|
||||
}
|
||||
})
|
||||
|
||||
if self._token and self._start_transcription:
|
||||
self.client.start_transcription(self.transcription_settings)
|
||||
@@ -227,39 +211,12 @@ class DailyTransportService(BaseTransportService, EventHandler):
|
||||
|
||||
def _post_run(self):
|
||||
self.client.leave()
|
||||
self.client.release()
|
||||
|
||||
def _handle_video_frame(self, participant_id, video_frame):
|
||||
"""If receive_video is true, this function is called once for each frame from each participant. We
|
||||
don't need to send every frame to the pipeline, so there are two ways to decide how to send frames:
|
||||
1. Set a greater-than-zero value for receive_video_fps. The transport will track the last send time
|
||||
for each participant and send a new frame when the requested frame rate has elapsed. This
|
||||
guarantees an image every second, for example.
|
||||
2. Set receive_video_fps less than or equal to zero to disable timed frame sending. Then, put a
|
||||
RequestVideoImageFrame in the pipeline to get a new frame for one or all participants. By
|
||||
sending a RequestVideoImageFrame immediately after successfully processing an image, you can
|
||||
ensure you don't end up queueing up frames faster than you can process them.
|
||||
"""
|
||||
send_frame = False
|
||||
if not participant_id in self._participant_frame_times:
|
||||
# then it's a new participant; send the first frame
|
||||
send_frame = True
|
||||
elif self._receive_video_fps > 0 and time.time() > self._participant_frame_times[participant_id] + 1.0/self._receive_video_fps:
|
||||
# Then it's an existing participant who is due to send a new frame
|
||||
send_frame = True
|
||||
|
||||
if send_frame:
|
||||
self._participant_frame_times[participant_id] = time.time()
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self.receive_queue.put(
|
||||
VideoImageFrame(participant_id, video_frame)), self._loop)
|
||||
|
||||
def on_first_other_participant_joined(self):
|
||||
pass
|
||||
|
||||
def call_joined(self, join_data, client_error):
|
||||
# self._logger.info(f"Call_joined: {join_data}, {client_error}")
|
||||
pass
|
||||
self._logger.info(f"Call_joined: {join_data}, {client_error}")
|
||||
|
||||
def dialout(self, number):
|
||||
self.client.start_dialout({"phoneNumber": number})
|
||||
@@ -277,21 +234,13 @@ class DailyTransportService(BaseTransportService, EventHandler):
|
||||
if not self._other_participant_has_joined and participant["id"] != self._my_participant_id:
|
||||
self._other_participant_has_joined = True
|
||||
self.on_first_other_participant_joined()
|
||||
if self._receive_video:
|
||||
self.client.set_video_renderer(
|
||||
participant["id"], self._handle_video_frame)
|
||||
|
||||
def on_participant_left(self, participant, reason):
|
||||
if len(self.client.participants()) < self._min_others_count + 1:
|
||||
self._stop_threads.set()
|
||||
|
||||
def on_app_message(self, message: Any, sender: str):
|
||||
if self._loop:
|
||||
frame = ReceivedAppMessageFrame(message, sender)
|
||||
print(frame)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.receive_queue.put(frame), self._loop
|
||||
)
|
||||
def on_app_message(self, message, sender):
|
||||
pass
|
||||
|
||||
def on_transcription_message(self, message: dict):
|
||||
if self._loop:
|
||||
@@ -301,16 +250,14 @@ class DailyTransportService(BaseTransportService, EventHandler):
|
||||
elif "session_id" in message:
|
||||
participantId = message["session_id"]
|
||||
if self._my_participant_id and participantId != self._my_participant_id:
|
||||
frame = TranscriptionQueueFrame(
|
||||
message["text"], participantId, message["timestamp"])
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.receive_queue.put(frame), self._loop)
|
||||
|
||||
def on_transcription_error(self, message):
|
||||
self._logger.error(f"Transcription error: {message}")
|
||||
|
||||
def on_transcription_started(self, status):
|
||||
pass
|
||||
frame = TranscriptionQueueFrame(message["text"], participantId, message["timestamp"])
|
||||
asyncio.run_coroutine_threadsafe(self.receive_queue.put(frame), self._loop)
|
||||
|
||||
def on_transcription_stopped(self, stopped_by, stopped_by_error):
|
||||
pass
|
||||
|
||||
def on_transcription_error(self, message):
|
||||
pass
|
||||
|
||||
def on_transcription_started(self, status):
|
||||
pass
|
||||
|
||||
@@ -25,9 +25,7 @@ class DeepgramAIService(TTSService):
|
||||
self.logger.info(f"Running deepgram tts for {sentence}")
|
||||
base_url = "https://api.beta.deepgram.com/v1/speak"
|
||||
request_url = f"{base_url}?model={self._voice}&encoding=linear16&container=none&sample_rate={self._sample_rate}"
|
||||
headers = {
|
||||
"authorization": f"token {self._api_key}",
|
||||
"Content-Type": "application/json"}
|
||||
headers = {"authorization": f"token {self._api_key}", "Content-Type": "application/json"}
|
||||
data = {"text": sentence}
|
||||
|
||||
async with self._aiohttp_session.post(
|
||||
|
||||
@@ -9,12 +9,7 @@ from dailyai.services.ai_services import TTSService
|
||||
|
||||
|
||||
class DeepgramTTSService(TTSService):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
aiohttp_session,
|
||||
api_key,
|
||||
voice="alpha-asteria-en-v2"):
|
||||
def __init__(self, *, aiohttp_session, api_key, voice="alpha-asteria-en-v2"):
|
||||
super().__init__()
|
||||
|
||||
self._voice = voice
|
||||
|
||||
@@ -15,28 +15,22 @@ class ElevenLabsTTSService(TTSService):
|
||||
*,
|
||||
aiohttp_session: aiohttp.ClientSession,
|
||||
api_key,
|
||||
narrator,
|
||||
model="eleven_turbo_v2",
|
||||
aggregate_sentences=True
|
||||
voice_id,
|
||||
):
|
||||
super().__init__(aggregate_sentences)
|
||||
super().__init__()
|
||||
|
||||
self._api_key = api_key
|
||||
self._narrator = narrator
|
||||
self._voice_id = voice_id
|
||||
self._aiohttp_session = aiohttp_session
|
||||
self._model = model
|
||||
|
||||
async def run_tts(self, sentence) -> AsyncGenerator[bytes, None]:
|
||||
url = f"https://api.elevenlabs.io/v1/text-to-speech/{self._narrator['narrator']['voice_id']}/stream"
|
||||
payload = {"text": sentence, "model_id": self._model}
|
||||
querystring = {
|
||||
"output_format": "pcm_16000",
|
||||
"optimize_streaming_latency": 2}
|
||||
url = f"https://api.elevenlabs.io/v1/text-to-speech/{self._voice_id}/stream"
|
||||
payload = {"text": sentence, "model_id": "eleven_turbo_v2"}
|
||||
querystring = {"output_format": "pcm_16000", "optimize_streaming_latency": 2}
|
||||
headers = {
|
||||
"xi-api-key": self._api_key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
async with self._aiohttp_session.post(
|
||||
url, json=payload, headers=headers, params=querystring
|
||||
) as r:
|
||||
|
||||
@@ -9,19 +9,17 @@ from dailyai.services.ai_services import ImageGenService
|
||||
|
||||
|
||||
from dailyai.services.ai_services import ImageGenService
|
||||
|
||||
# Fal expects FAL_KEY_ID and FAL_KEY_SECRET to be set in the env
|
||||
|
||||
|
||||
class FalImageGenService(ImageGenService):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
image_size,
|
||||
aiohttp_session: aiohttp.ClientSession,
|
||||
key_id=None,
|
||||
key_secret=None
|
||||
):
|
||||
self,
|
||||
*,
|
||||
image_size,
|
||||
aiohttp_session: aiohttp.ClientSession,
|
||||
key_id=None,
|
||||
key_secret=None):
|
||||
super().__init__(image_size)
|
||||
self._aiohttp_session = aiohttp_session
|
||||
if key_id:
|
||||
@@ -33,8 +31,9 @@ class FalImageGenService(ImageGenService):
|
||||
def get_image_url(sentence, size):
|
||||
handler = fal.apps.submit(
|
||||
"110602490-fast-sdxl",
|
||||
# "fal-ai/fast-sdxl",
|
||||
arguments={"prompt": sentence},
|
||||
arguments={
|
||||
"prompt": sentence
|
||||
},
|
||||
)
|
||||
for event in handler.iter_events():
|
||||
if isinstance(event, fal.apps.InProgress):
|
||||
@@ -47,13 +46,9 @@ class FalImageGenService(ImageGenService):
|
||||
raise Exception("Image generation failed")
|
||||
|
||||
return image_url
|
||||
|
||||
image_url = await asyncio.to_thread(get_image_url, sentence, self.image_size)
|
||||
# Load the image from the url
|
||||
async with self._aiohttp_session.get(image_url) as response:
|
||||
image_stream = io.BytesIO(await response.content.read())
|
||||
image = Image.open(image_stream)
|
||||
image_bytes = image.tobytes()
|
||||
print(f"!!! fal image tobytes is:")
|
||||
print(image)
|
||||
return (image_url, image_bytes)
|
||||
return (image_url, image.tobytes())
|
||||
|
||||
@@ -15,15 +15,13 @@ class LocalTransportService(BaseTransportService):
|
||||
self._tk_root = kwargs.get("tk_root") or None
|
||||
|
||||
if self._camera_enabled and not self._tk_root:
|
||||
raise ValueError(
|
||||
"If camera is enabled, a tkinter root must be provided")
|
||||
raise ValueError("If camera is enabled, a tkinter root must be provided")
|
||||
|
||||
if self._speaker_enabled:
|
||||
self._speaker_buffer_pending = bytearray()
|
||||
|
||||
async def _write_frame_to_tkinter(self, frame: bytes):
|
||||
data = f"P6 {self._camera_width} {self._camera_height} 255 ".encode() + \
|
||||
frame
|
||||
data = f"P6 {self._camera_width} {self._camera_height} 255 ".encode() + frame
|
||||
photo = tk.PhotoImage(
|
||||
width=self._camera_width,
|
||||
height=self._camera_height,
|
||||
@@ -31,8 +29,7 @@ class LocalTransportService(BaseTransportService):
|
||||
format="PPM")
|
||||
self._image_label.config(image=photo)
|
||||
|
||||
# This holds a reference to the photo, preventing it from being garbage
|
||||
# collected.
|
||||
# This holds a reference to the photo, preventing it from being garbage collected.
|
||||
self._image_label.image = photo # type: ignore
|
||||
|
||||
def write_frame_to_camera(self, frame: bytes):
|
||||
@@ -64,13 +61,8 @@ class LocalTransportService(BaseTransportService):
|
||||
if self._camera_enabled:
|
||||
# Start with a neutral gray background.
|
||||
array = np.ones((1024, 1024, 3)) * 128
|
||||
data = f"P5 {1024} {1024} 255 ".encode(
|
||||
) + array.astype(np.uint8).tobytes()
|
||||
photo = tk.PhotoImage(
|
||||
width=1024,
|
||||
height=1024,
|
||||
data=data,
|
||||
format="PPM")
|
||||
data = f"P5 {1024} {1024} 255 ".encode() + array.astype(np.uint8).tobytes()
|
||||
photo = tk.PhotoImage(width=1024, height=1024, data=data, format="PPM")
|
||||
self._image_label = tk.Label(self._tk_root, image=photo)
|
||||
self._image_label.pack()
|
||||
|
||||
|
||||
@@ -1,7 +1,44 @@
|
||||
from dailyai.services.openai_api_llm_service import BaseOpenAILLMService
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from dailyai.services.ai_services import LLMService
|
||||
|
||||
|
||||
class OLLamaLLMService(BaseOpenAILLMService):
|
||||
class OLLamaLLMService(LLMService):
|
||||
def __init__(self, model="llama2", base_url='http://localhost:11434/v1'):
|
||||
super().__init__()
|
||||
self._model = model
|
||||
self._client = AsyncOpenAI(api_key="ollama", base_url=base_url)
|
||||
|
||||
def __init__(self, model="llama2", base_url="http://localhost:11434/v1"):
|
||||
super().__init__(model=model, base_url=base_url, api_key="ollama")
|
||||
async def get_response(self, messages, stream):
|
||||
return await self._client.chat.completions.create(
|
||||
stream=stream,
|
||||
messages=messages,
|
||||
model=self._model
|
||||
)
|
||||
|
||||
async def run_llm_async(self, messages, tool_choice=None) -> AsyncGenerator[str, None]:
|
||||
messages_for_log = json.dumps(messages)
|
||||
self.logger.debug(f"Generating chat via openai: {messages_for_log}")
|
||||
|
||||
chunks = await self._client.chat.completions.create(
|
||||
model=self._model, stream=True, messages=messages
|
||||
)
|
||||
async for chunk in chunks:
|
||||
if len(chunk.choices) == 0:
|
||||
continue
|
||||
|
||||
if chunk.choices[0].delta.content:
|
||||
yield chunk.choices[0].delta.content
|
||||
|
||||
async def run_llm(self, messages) -> str | None:
|
||||
messages_for_log = json.dumps(messages)
|
||||
self.logger.debug(f"Generating chat via ollama: {messages_for_log}")
|
||||
|
||||
response = await self._client.chat.completions.create(model=self._model, stream=False, messages=messages)
|
||||
if response and len(response.choices) > 0:
|
||||
return response.choices[0].message.content
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -2,28 +2,55 @@ import aiohttp
|
||||
from PIL import Image
|
||||
import io
|
||||
import time
|
||||
import base64
|
||||
from openai import AsyncOpenAI, AsyncStream
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from openai.types.chat import (
|
||||
ChatCompletion,
|
||||
ChatCompletionChunk,
|
||||
ChatCompletionMessageParam,
|
||||
)
|
||||
|
||||
from daily import VideoFrame
|
||||
from dailyai.services.ai_services import LLMService, ImageGenService, VisionService
|
||||
from dailyai.services.openai_api_llm_service import BaseOpenAILLMService
|
||||
from dailyai.pipeline.frames import TextFrame
|
||||
from dailyai.services.ai_services import LLMService, ImageGenService
|
||||
|
||||
|
||||
class OpenAILLMService(BaseOpenAILLMService):
|
||||
class OpenAILLMService(LLMService):
|
||||
def __init__(self, *, api_key, model="gpt-4", tools=None, messages=None):
|
||||
super().__init__(tools=tools, messages=messages)
|
||||
self._model = model
|
||||
self._client = AsyncOpenAI(api_key=api_key)
|
||||
|
||||
def __init__(self, model="gpt-4", * args, **kwargs):
|
||||
super().__init__(model, *args, **kwargs)
|
||||
async def get_response(self, messages, stream):
|
||||
return await self._client.chat.completions.create(
|
||||
stream=stream,
|
||||
messages=messages,
|
||||
model=self._model,
|
||||
tools=self._tools
|
||||
)
|
||||
|
||||
async def run_llm_async(self, messages, tool_choice=None) -> AsyncGenerator[str, None]:
|
||||
messages_for_log = json.dumps(messages)
|
||||
self.logger.debug(f"Generating chat via openai: {messages_for_log}")
|
||||
if self._tools:
|
||||
tools = self._tools
|
||||
else:
|
||||
tools = None
|
||||
start_time = time.time()
|
||||
chunks = await self._client.chat.completions.create(model=self._model, stream=True, messages=messages, tools=tools, tool_choice=tool_choice)
|
||||
self.logger.info(f"=== OpenAI LLM TTFB: {time.time() - start_time}")
|
||||
async for chunk in chunks:
|
||||
if len(chunk.choices) == 0:
|
||||
continue
|
||||
if chunk.choices[0].delta.tool_calls:
|
||||
yield chunk.choices[0].delta.tool_calls[0]
|
||||
elif chunk.choices[0].delta.content:
|
||||
yield chunk.choices[0].delta.content
|
||||
|
||||
async def run_llm(self, messages) -> str | None:
|
||||
messages_for_log = json.dumps(messages)
|
||||
self.logger.debug(f"Generating chat via openai: {messages_for_log}")
|
||||
|
||||
response = await self._client.chat.completions.create(model=self._model, stream=False, messages=messages)
|
||||
if response and len(response.choices) > 0:
|
||||
return response.choices[0].message.content
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class OpenAIImageGenService(ImageGenService):
|
||||
@@ -59,67 +86,3 @@ class OpenAIImageGenService(ImageGenService):
|
||||
image_stream = io.BytesIO(await response.content.read())
|
||||
image = Image.open(image_stream)
|
||||
return (image_url, image.tobytes())
|
||||
|
||||
|
||||
class OpenAIVisionService(VisionService):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model="gpt-4-vision-preview",
|
||||
api_key,
|
||||
):
|
||||
self._model = model
|
||||
self._client = AsyncOpenAI(api_key=api_key)
|
||||
|
||||
async def run_vision(self, prompt: str, image: bytes):
|
||||
if isinstance(image, VideoFrame):
|
||||
# Then it's from a daily video frame
|
||||
print("### processing daily video frame for recognition")
|
||||
IMAGE_WIDTH = image.width
|
||||
IMAGE_HEIGHT = image.height
|
||||
COLOR_FORMAT = image.color_format
|
||||
a_image = Image.frombytes(
|
||||
'RGBA', (IMAGE_WIDTH, IMAGE_HEIGHT), image.buffer)
|
||||
new_image = a_image.convert('RGB')
|
||||
else:
|
||||
# handle it as a byte stream from image gen
|
||||
new_image = Image.frombytes('RGB', (1024, 1024), image)
|
||||
# Uncomment these lines to write the frame to a jpg in the same directory.
|
||||
# current_path = os.getcwd()
|
||||
# image_path = os.path.join(current_path, "image.jpg")
|
||||
# image.save(image_path, format="JPEG")
|
||||
|
||||
jpeg_buffer = io.BytesIO()
|
||||
|
||||
new_image.save(jpeg_buffer, format='JPEG')
|
||||
|
||||
jpeg_bytes = jpeg_buffer.getvalue()
|
||||
base64_image = base64.b64encode(jpeg_bytes).decode('utf-8')
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": prompt},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:image/jpeg;base64,{base64_image}"
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
chunks: AsyncStream[ChatCompletionChunk] = (
|
||||
await self._client.chat.completions.create(
|
||||
model=self._model,
|
||||
stream=True,
|
||||
messages=messages,
|
||||
)
|
||||
)
|
||||
async for chunk in chunks:
|
||||
print(f"%%% chunk: {chunk}")
|
||||
if len(chunk.choices) == 0:
|
||||
continue
|
||||
if chunk.choices[0].delta.content:
|
||||
yield TextFrame(chunk.choices[0].delta.content)
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import json
|
||||
import time
|
||||
from typing import AsyncGenerator, List
|
||||
from openai import AsyncOpenAI, AsyncStream
|
||||
from dailyai.pipeline.frames import (
|
||||
Frame,
|
||||
LLMFunctionCallFrame,
|
||||
LLMFunctionStartFrame,
|
||||
LLMMessagesQueueFrame,
|
||||
LLMResponseEndFrame,
|
||||
LLMResponseStartFrame,
|
||||
OpenAILLMContextFrame,
|
||||
TextFrame,
|
||||
)
|
||||
from dailyai.services.ai_services import LLMService
|
||||
from dailyai.services.openai_llm_context import OpenAILLMContext
|
||||
|
||||
from openai.types.chat import (
|
||||
ChatCompletion,
|
||||
ChatCompletionChunk,
|
||||
ChatCompletionMessageParam,
|
||||
)
|
||||
|
||||
|
||||
class BaseOpenAILLMService(LLMService):
|
||||
"""This is the base for all services that use the AsyncOpenAI client.
|
||||
|
||||
This service consumes OpenAILLMContextFrame frames, which contain a reference
|
||||
to an OpenAILLMContext frame. The OpenAILLMContext object defines the context
|
||||
sent to the LLM for a completion. This includes user, assistant and system messages
|
||||
as well as tool choices and the tool, which is used if requesting function
|
||||
calls from the LLM.
|
||||
"""
|
||||
|
||||
def __init__(self, model: str, api_key=None, base_url=None):
|
||||
super().__init__()
|
||||
self._model: str = model
|
||||
self.create_client(api_key=api_key, base_url=base_url)
|
||||
|
||||
def create_client(self, api_key=None, base_url=None):
|
||||
self._client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||
|
||||
async def _stream_chat_completions(
|
||||
self, context: OpenAILLMContext
|
||||
) -> AsyncStream[ChatCompletionChunk]:
|
||||
messages: List[ChatCompletionMessageParam] = context.get_messages()
|
||||
messages_for_log = json.dumps(messages)
|
||||
self.logger.debug(f"Generating chat via openai: {messages_for_log}")
|
||||
|
||||
start_time = time.time()
|
||||
chunks: AsyncStream[ChatCompletionChunk] = (
|
||||
await self._client.chat.completions.create(
|
||||
model=self._model,
|
||||
stream=True,
|
||||
messages=messages,
|
||||
tools=context.tools,
|
||||
tool_choice=context.tool_choice,
|
||||
)
|
||||
)
|
||||
self.logger.info(f"=== OpenAI LLM TTFB: {time.time() - start_time}")
|
||||
return chunks
|
||||
|
||||
async def _chat_completions(self, messages) -> str | None:
|
||||
messages_for_log = json.dumps(messages)
|
||||
self.logger.debug(f"Generating chat via openai: {messages_for_log}")
|
||||
|
||||
response: ChatCompletion = await self._client.chat.completions.create(
|
||||
model=self._model, stream=False, messages=messages
|
||||
)
|
||||
if response and len(response.choices) > 0:
|
||||
return response.choices[0].message.content
|
||||
else:
|
||||
return None
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, OpenAILLMContextFrame):
|
||||
context: OpenAILLMContext = frame.context
|
||||
elif isinstance(frame, LLMMessagesQueueFrame):
|
||||
context = OpenAILLMContext.from_messages(frame.messages)
|
||||
else:
|
||||
yield frame
|
||||
return
|
||||
|
||||
function_name = ""
|
||||
arguments = ""
|
||||
|
||||
yield LLMResponseStartFrame()
|
||||
chunk_stream: AsyncStream[ChatCompletionChunk] = (
|
||||
await self._stream_chat_completions(context)
|
||||
)
|
||||
async for chunk in chunk_stream:
|
||||
if len(chunk.choices) == 0:
|
||||
continue
|
||||
|
||||
if chunk.choices[0].delta.tool_calls:
|
||||
# We're streaming the LLM response to enable the fastest response times.
|
||||
# For text, we just yield each chunk as we receive it and count on consumers
|
||||
# to do whatever coalescing they need (eg. to pass full sentences to TTS)
|
||||
#
|
||||
# If the LLM is a function call, we'll do some coalescing here.
|
||||
# If the response contains a function name, we'll yield a frame to tell consumers
|
||||
# that they can start preparing to call the function with that name.
|
||||
# We accumulate all the arguments for the rest of the streamed response, then when
|
||||
# the response is done, we package up all the arguments and the function name and
|
||||
# yield a frame containing the function name and the arguments.
|
||||
|
||||
tool_call = chunk.choices[0].delta.tool_calls[0]
|
||||
if tool_call.function and tool_call.function.name:
|
||||
function_name += tool_call.function.name
|
||||
yield LLMFunctionStartFrame(function_name=tool_call.function.name)
|
||||
if tool_call.function and tool_call.function.arguments:
|
||||
# Keep iterating through the response to collect all the argument fragments and
|
||||
# yield a complete LLMFunctionCallFrame after run_llm_async
|
||||
# completes
|
||||
arguments += tool_call.function.arguments
|
||||
elif chunk.choices[0].delta.content:
|
||||
yield TextFrame(chunk.choices[0].delta.content)
|
||||
|
||||
# if we got a function name and arguments, yield the frame with all the info so
|
||||
# frame consumers can take action based on the function call.
|
||||
if function_name and arguments:
|
||||
yield LLMFunctionCallFrame(function_name=function_name, arguments=arguments)
|
||||
|
||||
yield LLMResponseEndFrame()
|
||||
@@ -1,54 +0,0 @@
|
||||
from typing import List
|
||||
from openai._types import NOT_GIVEN, NotGiven
|
||||
|
||||
from openai.types.chat import (
|
||||
ChatCompletionToolParam,
|
||||
ChatCompletionToolChoiceOptionParam,
|
||||
ChatCompletionMessageParam,
|
||||
)
|
||||
|
||||
|
||||
class OpenAILLMContext:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
messages: List[ChatCompletionMessageParam] | None = None,
|
||||
tools: List[ChatCompletionToolParam] | NotGiven = NOT_GIVEN,
|
||||
tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = NOT_GIVEN
|
||||
):
|
||||
self.messages: List[ChatCompletionMessageParam] = messages if messages else [
|
||||
]
|
||||
self.tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = tool_choice
|
||||
self.tools: List[ChatCompletionToolParam] | NotGiven = tools
|
||||
|
||||
@staticmethod
|
||||
def from_messages(messages: List[dict]) -> "OpenAILLMContext":
|
||||
context = OpenAILLMContext()
|
||||
for message in messages:
|
||||
context.add_message({
|
||||
"content": message["content"],
|
||||
"role": message["role"],
|
||||
"name": message["name"] if "name" in message else message["role"]
|
||||
})
|
||||
return context
|
||||
|
||||
# def __deepcopy__(self, memo):
|
||||
|
||||
def add_message(self, message: ChatCompletionMessageParam):
|
||||
self.messages.append(message)
|
||||
|
||||
def get_messages(self) -> List[ChatCompletionMessageParam]:
|
||||
return self.messages
|
||||
|
||||
def set_tool_choice(
|
||||
self, tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven
|
||||
):
|
||||
self.tool_choice = tool_choice
|
||||
|
||||
def set_tools(
|
||||
self,
|
||||
tools: List[ChatCompletionToolParam] | NotGiven = NOT_GIVEN):
|
||||
if tools != NOT_GIVEN and len(tools) == 0:
|
||||
tools = NOT_GIVEN
|
||||
|
||||
self.tools = tools
|
||||
@@ -17,10 +17,7 @@ class CloudflareAIService(AIService):
|
||||
|
||||
# base endpoint, used by the others
|
||||
def run(self, model, input):
|
||||
response = requests.post(
|
||||
f"{self.api_base_url}{model}",
|
||||
headers=self.headers,
|
||||
json=input)
|
||||
response = requests.post(f"{self.api_base_url}{model}", headers=self.headers, json=input)
|
||||
return response.json()
|
||||
|
||||
# https://developers.cloudflare.com/workers-ai/models/llm/
|
||||
@@ -44,8 +41,7 @@ class CloudflareAIService(AIService):
|
||||
|
||||
# https://developers.cloudflare.com/workers-ai/models/sentiment-analysis/
|
||||
def run_text_sentiment(self, sentence):
|
||||
return self.run("@cf/huggingface/distilbert-sst-2-int8",
|
||||
{"text": sentence})
|
||||
return self.run("@cf/huggingface/distilbert-sst-2-int8", {"text": sentence})
|
||||
|
||||
# https://developers.cloudflare.com/workers-ai/models/image-classification/
|
||||
def run_image_classification(self, image_url):
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from dailyai.pipeline.frames import (
|
||||
OpenAILLMContextFrame,
|
||||
)
|
||||
from dailyai.services.azure_ai_services import AzureLLMService
|
||||
from dailyai.services.openai_llm_context import OpenAILLMContext
|
||||
|
||||
from openai.types.chat import (
|
||||
ChatCompletionSystemMessageParam,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
async def test_chat():
|
||||
llm = AzureLLMService(
|
||||
api_key=os.getenv("AZURE_CHATGPT_API_KEY"),
|
||||
endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"),
|
||||
model=os.getenv("AZURE_CHATGPT_MODEL"),
|
||||
)
|
||||
context = OpenAILLMContext()
|
||||
message: ChatCompletionSystemMessageParam = ChatCompletionSystemMessageParam(
|
||||
content="Please tell the world hello.", name="system", role="system")
|
||||
context.add_message(message)
|
||||
frame = OpenAILLMContextFrame(context)
|
||||
async for s in llm.process_frame(frame):
|
||||
print(s)
|
||||
|
||||
asyncio.run(test_chat())
|
||||
@@ -1,23 +0,0 @@
|
||||
import asyncio
|
||||
from dailyai.pipeline.frames import (
|
||||
OpenAILLMContextFrame,
|
||||
)
|
||||
from dailyai.services.openai_llm_context import OpenAILLMContext
|
||||
|
||||
from openai.types.chat import (
|
||||
ChatCompletionSystemMessageParam,
|
||||
)
|
||||
from dailyai.services.ollama_ai_services import OLLamaLLMService
|
||||
|
||||
if __name__ == "__main__":
|
||||
async def test_chat():
|
||||
llm = OLLamaLLMService()
|
||||
context = OpenAILLMContext()
|
||||
message: ChatCompletionSystemMessageParam = ChatCompletionSystemMessageParam(
|
||||
content="Please tell the world hello.", name="system", role="system")
|
||||
context.add_message(message)
|
||||
frame = OpenAILLMContextFrame(context)
|
||||
async for s in llm.process_frame(frame):
|
||||
print(s)
|
||||
|
||||
asyncio.run(test_chat())
|
||||
@@ -1,85 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from dailyai.pipeline.frames import (
|
||||
OpenAILLMContextFrame,
|
||||
)
|
||||
from dailyai.services.openai_llm_context import OpenAILLMContext
|
||||
|
||||
from openai.types.chat import (
|
||||
ChatCompletionSystemMessageParam,
|
||||
ChatCompletionToolParam,
|
||||
ChatCompletionUserMessageParam,
|
||||
)
|
||||
|
||||
from dailyai.services.openai_api_llm_service import BaseOpenAILLMService
|
||||
|
||||
if __name__ == "__main__":
|
||||
async def test_functions():
|
||||
tools = [
|
||||
ChatCompletionToolParam(
|
||||
type="function",
|
||||
function={
|
||||
"name": "get_current_weather",
|
||||
"description": "Get the current weather",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The city and state, e.g. San Francisco, CA",
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"celsius",
|
||||
"fahrenheit"],
|
||||
"description": "The temperature unit to use. Infer this from the users location.",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"location",
|
||||
"format"],
|
||||
},
|
||||
})]
|
||||
|
||||
api_key = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
llm = BaseOpenAILLMService(
|
||||
api_key=api_key or "",
|
||||
model="gpt-4-1106-preview",
|
||||
)
|
||||
context = OpenAILLMContext(tools=tools)
|
||||
system_message: ChatCompletionSystemMessageParam = ChatCompletionSystemMessageParam(
|
||||
content="Ask the user to ask for a weather report", name="system", role="system"
|
||||
)
|
||||
user_message: ChatCompletionUserMessageParam = ChatCompletionUserMessageParam(
|
||||
content="Could you tell me the weather for Boulder, Colorado",
|
||||
name="user",
|
||||
role="user",
|
||||
)
|
||||
context.add_message(system_message)
|
||||
context.add_message(user_message)
|
||||
frame = OpenAILLMContextFrame(context)
|
||||
async for s in llm.process_frame(frame):
|
||||
print(s)
|
||||
|
||||
async def test_chat():
|
||||
api_key = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
llm = BaseOpenAILLMService(
|
||||
api_key=api_key or "",
|
||||
model="gpt-4-1106-preview",
|
||||
)
|
||||
context = OpenAILLMContext()
|
||||
message: ChatCompletionSystemMessageParam = ChatCompletionSystemMessageParam(
|
||||
content="Please tell the world hello.", name="system", role="system")
|
||||
context.add_message(message)
|
||||
frame = OpenAILLMContextFrame(context)
|
||||
async for s in llm.process_frame(frame):
|
||||
print(s)
|
||||
|
||||
async def run_tests():
|
||||
await test_functions()
|
||||
await test_chat()
|
||||
|
||||
asyncio.run(run_tests())
|
||||
@@ -45,9 +45,10 @@ class TestDailyFrameAggregators(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
async def test_gated_accumulator(self):
|
||||
gated_aggregator = GatedAggregator(
|
||||
gate_open_fn=lambda frame: isinstance(
|
||||
frame, ImageFrame), gate_close_fn=lambda frame: isinstance(
|
||||
frame, LLMResponseStartFrame), start_open=False, )
|
||||
gate_open_fn=lambda frame: isinstance(frame, ImageFrame),
|
||||
gate_close_fn=lambda frame: isinstance(frame, LLMResponseStartFrame),
|
||||
start_open=False,
|
||||
)
|
||||
|
||||
frames = [
|
||||
LLMResponseStartFrame(),
|
||||
@@ -75,14 +76,12 @@ class TestDailyFrameAggregators(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
async def test_parallel_pipeline(self):
|
||||
|
||||
async def slow_add(sleep_time: float, name: str, x: str):
|
||||
async def slow_add(sleep_time:float, name:str, x: str):
|
||||
await asyncio.sleep(sleep_time)
|
||||
return ":".join([x, name])
|
||||
|
||||
pipe1_annotation = StatelessTextTransformer(
|
||||
functools.partial(slow_add, 0.1, 'pipe1'))
|
||||
pipe2_annotation = StatelessTextTransformer(
|
||||
functools.partial(slow_add, 0.2, 'pipe2'))
|
||||
pipe1_annotation = StatelessTextTransformer(functools.partial(slow_add, 0.1, 'pipe1'))
|
||||
pipe2_annotation = StatelessTextTransformer(functools.partial(slow_add, 0.2, 'pipe2'))
|
||||
sentence_aggregator = SentenceAggregator()
|
||||
add_dots = StatelessTextTransformer(lambda x: x + ".")
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class SimpleAIService(AIService):
|
||||
|
||||
|
||||
class TestBaseAIService(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_simple_processing(self):
|
||||
async def test_async_input(self):
|
||||
service = SimpleAIService()
|
||||
|
||||
input_frames = [
|
||||
@@ -20,10 +20,28 @@ class TestBaseAIService(unittest.IsolatedAsyncioTestCase):
|
||||
EndFrame()
|
||||
]
|
||||
|
||||
async def iterate_frames() -> AsyncGenerator[Frame, None]:
|
||||
for frame in input_frames:
|
||||
yield frame
|
||||
|
||||
output_frames = []
|
||||
for input_frame in input_frames:
|
||||
async for output_frame in service.process_frame(input_frame):
|
||||
output_frames.append(output_frame)
|
||||
async for frame in service.run(iterate_frames()):
|
||||
output_frames.append(frame)
|
||||
|
||||
self.assertEqual(input_frames, output_frames)
|
||||
|
||||
async def test_nonasync_input(self):
|
||||
service = SimpleAIService()
|
||||
|
||||
input_frames = [TextFrame("hello"), EndFrame()]
|
||||
|
||||
def iterate_frames() -> Generator[Frame, None, None]:
|
||||
for frame in input_frames:
|
||||
yield frame
|
||||
|
||||
output_frames = []
|
||||
async for frame in service.run(iterate_frames()):
|
||||
output_frames.append(frame)
|
||||
|
||||
self.assertEqual(input_frames, output_frames)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import threading
|
||||
import unittest
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -25,8 +24,6 @@ class TestDailyTransport(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.assertTrue(was_called)
|
||||
|
||||
"""
|
||||
TODO: fix this test, it broke when I added the `.result` call in the patch.
|
||||
async def test_event_handler_async(self):
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
|
||||
@@ -37,19 +34,13 @@ class TestDailyTransport(unittest.IsolatedAsyncioTestCase):
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def test_event_handler(transport):
|
||||
nonlocal event
|
||||
print("sleeping")
|
||||
await asyncio.sleep(0.1)
|
||||
print("setting")
|
||||
event.set()
|
||||
print("returning")
|
||||
|
||||
thread = threading.Thread(target=transport.on_first_other_participant_joined)
|
||||
thread.start()
|
||||
thread.join()
|
||||
transport.on_first_other_participant_joined()
|
||||
|
||||
await asyncio.wait_for(event.wait(), timeout=1)
|
||||
self.assertTrue(event.is_set())
|
||||
"""
|
||||
|
||||
"""
|
||||
@patch("dailyai.services.daily_transport_service.CallClient")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
from doctest import OutputChecker
|
||||
import unittest
|
||||
from dailyai.pipeline.aggregators import SentenceAggregator, StatelessTextTransformer
|
||||
from dailyai.pipeline.frames import EndFrame, TextFrame
|
||||
|
||||
@@ -1,49 +1,62 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import os
|
||||
from dailyai.pipeline.frames import EndFrame, TextFrame
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.playht_ai_service import PlayHTAIService
|
||||
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
from examples.foundational.support.runner import configure
|
||||
|
||||
|
||||
async def main(room_url):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# create a transport service object using environment variables for
|
||||
# the transport service's API key, room url, and any other configuration.
|
||||
# services can all define and document the environment variables they use.
|
||||
# services all also take an optional config object that is used instead of
|
||||
# environment variables.
|
||||
#
|
||||
# the abstract transport service APIs presumably can map pretty closely
|
||||
# to the daily-python basic API
|
||||
meeting_duration_minutes = 5
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
None,
|
||||
"Say One Thing",
|
||||
mic_enabled=True,
|
||||
meeting_duration_minutes,
|
||||
mic_enabled=True
|
||||
)
|
||||
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"),
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"))
|
||||
"""
|
||||
tts = PlayHTAIService(
|
||||
api_key=os.getenv("PLAY_HT_API_KEY"),
|
||||
user_id=os.getenv("PLAY_HT_USER_ID"),
|
||||
voice_url=os.getenv("PLAY_HT_VOICE_URL"),
|
||||
)
|
||||
"""
|
||||
|
||||
pipeline = Pipeline([tts])
|
||||
|
||||
# Register an event handler so we can play the audio when the
|
||||
# participant joins.
|
||||
# Register an event handler so we can play the audio when the participant joins.
|
||||
@transport.event_handler("on_participant_joined")
|
||||
async def on_participant_joined(transport, participant):
|
||||
nonlocal tts
|
||||
if participant["info"]["isLocal"]:
|
||||
return
|
||||
|
||||
participant_name = participant["info"]["userName"] or ''
|
||||
await pipeline.queue_frames([TextFrame("Hello there, " + participant_name + "!"), EndFrame()])
|
||||
await tts.say(
|
||||
"Hello there, " + participant["info"]["userName"] + "!",
|
||||
transport.send_queue,
|
||||
)
|
||||
|
||||
await transport.run(pipeline)
|
||||
del tts
|
||||
# wait for the output queue to be empty, then leave the meeting
|
||||
await transport.stop_when_done()
|
||||
|
||||
await transport.run()
|
||||
del(tts)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import os
|
||||
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.local_transport_service import LocalTransportService
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
async def main():
|
||||
async with aiohttp.ClientSession() as session:
|
||||
meeting_duration_minutes = 1
|
||||
transport = LocalTransportService(
|
||||
duration_minutes=meeting_duration_minutes, mic_enabled=True
|
||||
duration_minutes=meeting_duration_minutes,
|
||||
mic_enabled=True
|
||||
)
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
|
||||
@@ -1,54 +1,57 @@
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
|
||||
from dailyai.pipeline.frames import EndFrame, LLMMessagesQueueFrame
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.pipeline.frames import LLMMessagesQueueFrame
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.azure_ai_services import AzureLLMService, AzureTTSService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.deepgram_ai_services import DeepgramTTSService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService
|
||||
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
from examples.foundational.support.runner import configure
|
||||
|
||||
|
||||
async def main(room_url):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
meeting_duration_minutes = 1
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
None,
|
||||
"Say One Thing From an LLM",
|
||||
mic_enabled=True,
|
||||
duration_minutes=meeting_duration_minutes,
|
||||
mic_enabled=True
|
||||
)
|
||||
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"),
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"))
|
||||
# tts = AzureTTSService(api_key=os.getenv("AZURE_SPEECH_API_KEY"), region=os.getenv("AZURE_SPEECH_REGION"))
|
||||
# tts = DeepgramTTSService(aiohttp_session=session, api_key=os.getenv("DEEPGRAM_API_KEY"), voice=os.getenv("DEEPGRAM_VOICE"))
|
||||
|
||||
llm = AzureLLMService(
|
||||
api_key=os.getenv("AZURE_CHATGPT_API_KEY"),
|
||||
endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"),
|
||||
model=os.getenv("AZURE_CHATGPT_MODEL"))
|
||||
# llm = OpenAILLMService(api_key=os.getenv("OPENAI_CHATGPT_API_KEY"))
|
||||
messages = [{
|
||||
"role": "system",
|
||||
"content": "You are an LLM in a WebRTC session, and this is a 'hello world' demo. Say hello to the world."
|
||||
}]
|
||||
tts_task = asyncio.create_task(
|
||||
tts.run_to_queue(
|
||||
transport.send_queue,
|
||||
llm.run([LLMMessagesQueueFrame(messages)]),
|
||||
)
|
||||
)
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an LLM in a WebRTC session, and this is a 'hello world' demo. Say hello to the world.",
|
||||
}]
|
||||
|
||||
pipeline = Pipeline([llm, tts])
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await pipeline.queue_frames([LLMMessagesQueueFrame(messages), EndFrame()])
|
||||
await tts_task
|
||||
await transport.stop_when_done()
|
||||
|
||||
await transport.run(pipeline)
|
||||
await transport.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import os
|
||||
|
||||
from dailyai.pipeline.frames import EndFrame, TextFrame
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.pipeline.frames import TextFrame
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.fal_ai_services import FalImageGenService
|
||||
from dailyai.services.open_ai_services import OpenAIImageGenService
|
||||
from dailyai.services.azure_ai_services import AzureImageGenServiceREST
|
||||
|
||||
from examples.support.runner import configure
|
||||
from examples.foundational.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
local_joined = False
|
||||
participant_joined = False
|
||||
|
||||
|
||||
async def main(room_url):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
meeting_duration_minutes = 1
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
None,
|
||||
"Show a still frame image",
|
||||
duration_minutes=meeting_duration_minutes,
|
||||
mic_enabled=False,
|
||||
camera_enabled=True,
|
||||
camera_width=1024,
|
||||
camera_height=1024,
|
||||
duration_minutes=1
|
||||
camera_height=1024
|
||||
)
|
||||
|
||||
imagegen = FalImageGenService(
|
||||
image_size="square_hd",
|
||||
image_size="1024x1024",
|
||||
aiohttp_session=session,
|
||||
key_id=os.getenv("FAL_KEY_ID"),
|
||||
key_secret=os.getenv("FAL_KEY_SECRET"),
|
||||
)
|
||||
key_secret=os.getenv("FAL_KEY_SECRET"))
|
||||
# imagegen = OpenAIImageGenService(aiohttp_session=session, api_key=os.getenv("OPENAI_DALLE_API_KEY"), image_size="1024x1024")
|
||||
# imagegen = AzureImageGenServiceREST(image_size="1024x1024", aiohttp_session=session, api_key=os.getenv("AZURE_DALLE_API_KEY"), endpoint=os.getenv("AZURE_DALLE_ENDPOINT"), model=os.getenv("AZURE_DALLE_MODEL"))
|
||||
|
||||
pipeline = Pipeline([imagegen])
|
||||
image_task = asyncio.create_task(
|
||||
imagegen.run_to_queue(
|
||||
transport.send_queue, [
|
||||
TextFrame("a cat in the style of picasso")]))
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
# Note that we do not put an EndFrame() item in the pipeline for this demo.
|
||||
# This means that the bot will stay in the channel until it times out.
|
||||
# An EndFrame() in the pipeline would cause the transport to shut
|
||||
# down.
|
||||
await pipeline.queue_frames(
|
||||
[TextFrame("a cat in the style of picasso")]
|
||||
)
|
||||
await image_task
|
||||
|
||||
await transport.run(pipeline)
|
||||
await transport.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import os
|
||||
|
||||
import tkinter as tk
|
||||
@@ -9,10 +8,6 @@ from dailyai.pipeline.frames import TextFrame
|
||||
from dailyai.services.fal_ai_services import FalImageGenService
|
||||
from dailyai.services.local_transport_service import LocalTransportService
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
local_joined = False
|
||||
participant_joined = False
|
||||
|
||||
@@ -39,8 +34,9 @@ async def main():
|
||||
)
|
||||
image_task = asyncio.create_task(
|
||||
imagegen.run_to_queue(
|
||||
transport.send_queue, [
|
||||
TextFrame("a cat in the style of picasso")]))
|
||||
transport.send_queue, [TextFrame("a cat in the style of picasso")]
|
||||
)
|
||||
)
|
||||
|
||||
async def run_tk():
|
||||
while not transport._stop_threads.is_set():
|
||||
@@ -50,6 +46,5 @@ async def main():
|
||||
|
||||
await asyncio.gather(transport.run(), image_task, run_tk())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import aiohttp
|
||||
from dailyai.pipeline.merge_pipeline import SequentialMergePipeline
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.azure_ai_services import AzureLLMService, AzureTTSService
|
||||
from dailyai.services.deepgram_ai_services import DeepgramTTSService
|
||||
from dailyai.pipeline.frames import EndFrame, EndPipeFrame, LLMMessagesQueueFrame, TextFrame
|
||||
from dailyai.pipeline.frames import EndFrame, LLMMessagesQueueFrame
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
from examples.foundational.support.runner import configure
|
||||
|
||||
|
||||
async def main(room_url: str):
|
||||
@@ -27,53 +21,49 @@ async def main(room_url: str):
|
||||
duration_minutes=1,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=False
|
||||
)
|
||||
|
||||
llm = AzureLLMService(
|
||||
api_key=os.getenv("AZURE_CHATGPT_API_KEY"),
|
||||
endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"),
|
||||
model=os.getenv("AZURE_CHATGPT_MODEL"),
|
||||
)
|
||||
model=os.getenv("AZURE_CHATGPT_MODEL"))
|
||||
azure_tts = AzureTTSService(
|
||||
api_key=os.getenv("AZURE_SPEECH_API_KEY"),
|
||||
region=os.getenv("AZURE_SPEECH_REGION"),
|
||||
)
|
||||
|
||||
deepgram_tts = DeepgramTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("DEEPGRAM_API_KEY"),
|
||||
)
|
||||
region=os.getenv("AZURE_SPEECH_REGION"))
|
||||
elevenlabs_tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"),
|
||||
)
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"))
|
||||
|
||||
messages = [{"role": "system",
|
||||
"content": "tell the user a joke about llamas"}]
|
||||
messages = [{"role": "system", "content": "tell the user a joke about llamas"}]
|
||||
|
||||
# Start a task to run the LLM to create a joke, and convert the LLM output to audio frames. This task
|
||||
# will run in parallel with generating and speaking the audio for static text, so there's no delay to
|
||||
# speak the LLM response.
|
||||
llm_pipeline = Pipeline([llm, elevenlabs_tts])
|
||||
await llm_pipeline.queue_frames([LLMMessagesQueueFrame(messages), EndPipeFrame()])
|
||||
buffer_queue = asyncio.Queue()
|
||||
source_queue = asyncio.Queue()
|
||||
pipeline = Pipeline(source = source_queue, sink=buffer_queue, processors=[llm, elevenlabs_tts])
|
||||
source_queue.put_nowait(LLMMessagesQueueFrame(messages))
|
||||
pipeline_run_task = pipeline.run_pipeline()
|
||||
|
||||
simple_tts_pipeline = Pipeline([azure_tts])
|
||||
await simple_tts_pipeline.queue_frames(
|
||||
[
|
||||
TextFrame("My friend the LLM is going to tell a joke about llamas"),
|
||||
EndPipeFrame(),
|
||||
]
|
||||
)
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await azure_tts.say("My friend the LLM is now going to tell a joke about llamas.", transport.send_queue)
|
||||
|
||||
merge_pipeline = SequentialMergePipeline(
|
||||
[simple_tts_pipeline, llm_pipeline])
|
||||
async def buffer_to_send_queue():
|
||||
while True:
|
||||
frame = await buffer_queue.get()
|
||||
await transport.send_queue.put(frame)
|
||||
buffer_queue.task_done()
|
||||
if isinstance(frame, EndFrame):
|
||||
break
|
||||
|
||||
await asyncio.gather(
|
||||
transport.run(merge_pipeline),
|
||||
simple_tts_pipeline.run_pipeline(),
|
||||
llm_pipeline.run_pipeline(),
|
||||
)
|
||||
await asyncio.gather(pipeline_run_task, buffer_to_send_queue())
|
||||
|
||||
await transport.stop_when_done()
|
||||
|
||||
await transport.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,142 +2,91 @@ import asyncio
|
||||
from re import S
|
||||
import aiohttp
|
||||
import os
|
||||
import logging
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from dailyai.pipeline.aggregators import (
|
||||
GatedAggregator,
|
||||
LLMFullResponseAggregator,
|
||||
ParallelPipeline,
|
||||
SentenceAggregator,
|
||||
)
|
||||
from dailyai.pipeline.frames import (
|
||||
Frame,
|
||||
TextFrame,
|
||||
EndFrame,
|
||||
ImageFrame,
|
||||
LLMMessagesQueueFrame,
|
||||
LLMResponseStartFrame,
|
||||
)
|
||||
from dailyai.pipeline.frame_processor import FrameProcessor
|
||||
from dailyai.pipeline.aggregators import GatedAggregator, LLMFullResponseAggregator, ParallelPipeline, SentenceAggregator
|
||||
|
||||
from dailyai.pipeline.frames import AudioFrame, EndFrame, ImageFrame, LLMMessagesQueueFrame, LLMResponseStartFrame
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService
|
||||
from dailyai.services.azure_ai_services import AzureLLMService, AzureImageGenServiceREST, AzureTTSService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.fal_ai_services import FalImageGenService
|
||||
from dailyai.services.open_ai_services import OpenAIImageGenService
|
||||
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MonthFrame(Frame):
|
||||
month: str
|
||||
|
||||
|
||||
class MonthPrepender(FrameProcessor):
|
||||
def __init__(self):
|
||||
self.most_recent_month = "Placeholder, month frame not yet received"
|
||||
self.prepend_to_next_text_frame = False
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, MonthFrame):
|
||||
self.most_recent_month = frame.month
|
||||
elif self.prepend_to_next_text_frame and isinstance(frame, TextFrame):
|
||||
yield TextFrame(f"{self.most_recent_month}: {frame.text}")
|
||||
self.prepend_to_next_text_frame = False
|
||||
elif isinstance(frame, LLMResponseStartFrame):
|
||||
self.prepend_to_next_text_frame = True
|
||||
yield frame
|
||||
else:
|
||||
yield frame
|
||||
from examples.foundational.support.runner import configure
|
||||
|
||||
|
||||
async def main(room_url):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
meeting_duration_minutes = 5
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
None,
|
||||
"Month Narration Bot",
|
||||
duration_minutes=meeting_duration_minutes,
|
||||
mic_enabled=True,
|
||||
camera_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_width=1024,
|
||||
camera_height=1024,
|
||||
camera_height=1024
|
||||
)
|
||||
|
||||
llm = AzureLLMService(
|
||||
api_key=os.getenv("AZURE_CHATGPT_API_KEY"),
|
||||
endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"),
|
||||
model=os.getenv("AZURE_CHATGPT_MODEL"))
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"),
|
||||
)
|
||||
voice_id="ErXwobaYiN019PkySvjV")
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
|
||||
imagegen = FalImageGenService(
|
||||
image_size="square_hd",
|
||||
dalle = FalImageGenService(
|
||||
image_size="1024x1024",
|
||||
aiohttp_session=session,
|
||||
key_id=os.getenv("FAL_KEY_ID"),
|
||||
key_secret=os.getenv("FAL_KEY_SECRET"),
|
||||
)
|
||||
key_secret=os.getenv("FAL_KEY_SECRET"))
|
||||
|
||||
gated_aggregator = GatedAggregator(
|
||||
gate_open_fn=lambda frame: isinstance(
|
||||
frame, ImageFrame), gate_close_fn=lambda frame: isinstance(
|
||||
frame, LLMResponseStartFrame), start_open=False, )
|
||||
source_queue = asyncio.Queue()
|
||||
|
||||
sentence_aggregator = SentenceAggregator()
|
||||
month_prepender = MonthPrepender()
|
||||
llm_full_response_aggregator = LLMFullResponseAggregator()
|
||||
|
||||
pipeline = Pipeline(
|
||||
processors=[
|
||||
llm,
|
||||
sentence_aggregator,
|
||||
ParallelPipeline(
|
||||
[[month_prepender, tts], [llm_full_response_aggregator, imagegen]]
|
||||
),
|
||||
gated_aggregator,
|
||||
],
|
||||
)
|
||||
|
||||
frames = []
|
||||
for month in [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
]:
|
||||
for month in ["January", "February"]:
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"Describe a nature photograph suitable for use in a calendar, for the month of {month}. Include only the image description with no preamble. Limit the description to one sentence, please.",
|
||||
}
|
||||
]
|
||||
frames.append(MonthFrame(month))
|
||||
frames.append(LLMMessagesQueueFrame(messages))
|
||||
await source_queue.put(LLMMessagesQueueFrame(messages))
|
||||
|
||||
frames.append(EndFrame())
|
||||
await pipeline.queue_frames(frames)
|
||||
await source_queue.put(EndFrame())
|
||||
|
||||
await transport.run(pipeline, override_pipeline_source_queue=False)
|
||||
gated_aggregator = GatedAggregator(
|
||||
gate_open_fn=lambda frame: isinstance(frame, ImageFrame),
|
||||
gate_close_fn=lambda frame: isinstance(frame, LLMResponseStartFrame),
|
||||
start_open=False,
|
||||
)
|
||||
|
||||
sentence_aggregator = SentenceAggregator()
|
||||
llm_full_response_aggregator = LLMFullResponseAggregator()
|
||||
|
||||
pipeline = Pipeline(
|
||||
source=source_queue,
|
||||
sink=transport.send_queue,
|
||||
processors=[
|
||||
llm,
|
||||
sentence_aggregator,
|
||||
ParallelPipeline([[tts], [llm_full_response_aggregator, dalle]]),
|
||||
gated_aggregator,
|
||||
],
|
||||
)
|
||||
pipeline_task = pipeline.run_pipeline()
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await pipeline_task
|
||||
|
||||
# wait for the output queue to be empty, then leave the meeting
|
||||
await transport.stop_when_done()
|
||||
|
||||
await transport.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
(url, token) = configure()
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import aiohttp
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import tkinter as tk
|
||||
import os
|
||||
|
||||
from dailyai.pipeline.frames import AudioFrame, ImageFrame
|
||||
from dailyai.services.open_ai_services import OpenAILLMService
|
||||
from dailyai.services.azure_ai_services import AzureLLMService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.fal_ai_services import FalImageGenService
|
||||
from dailyai.services.local_transport_service import LocalTransportService
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
async def main(room_url):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
@@ -31,16 +26,16 @@ async def main(room_url):
|
||||
tk_root=tk_root,
|
||||
)
|
||||
|
||||
llm = AzureLLMService(
|
||||
api_key=os.getenv("AZURE_CHATGPT_API_KEY"),
|
||||
endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"),
|
||||
model=os.getenv("AZURE_CHATGPT_MODEL"),
|
||||
)
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"),
|
||||
voice_id="ErXwobaYiN019PkySvjV",
|
||||
)
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
|
||||
dalle = FalImageGenService(
|
||||
image_size="1024x1024",
|
||||
aiohttp_session=session,
|
||||
@@ -49,8 +44,7 @@ async def main(room_url):
|
||||
)
|
||||
|
||||
# Get a complete audio chunk from the given text. Splitting this into its own
|
||||
# coroutine lets us ensure proper ordering of the audio chunks on the
|
||||
# send queue.
|
||||
# coroutine lets us ensure proper ordering of the audio chunks on the send queue.
|
||||
async def get_all_audio(text):
|
||||
all_audio = bytearray()
|
||||
async for audio in tts.run_tts(text):
|
||||
@@ -72,9 +66,10 @@ async def main(room_url):
|
||||
|
||||
to_speak = f"{month}: {image_description}"
|
||||
audio_task = asyncio.create_task(get_all_audio(to_speak))
|
||||
image_task = asyncio.create_task(
|
||||
dalle.run_image_gen(image_description))
|
||||
(audio, image_data) = await asyncio.gather(audio_task, image_task)
|
||||
image_task = asyncio.create_task(dalle.run_image_gen(image_description))
|
||||
(audio, image_data) = await asyncio.gather(
|
||||
audio_task, image_task
|
||||
)
|
||||
|
||||
return {
|
||||
"month": month,
|
||||
@@ -102,8 +97,7 @@ async def main(room_url):
|
||||
async def show_images():
|
||||
# This will play the months in the order they're completed. The benefit
|
||||
# is we'll have as little delay as possible before the first month, and
|
||||
# likely no delay between months, but the months won't display in
|
||||
# order.
|
||||
# likely no delay between months, but the months won't display in order.
|
||||
for month_data_task in asyncio.as_completed(month_tasks):
|
||||
data = await month_data_task
|
||||
if data:
|
||||
@@ -125,21 +119,15 @@ async def main(room_url):
|
||||
tk_root.update_idletasks()
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
month_tasks = [
|
||||
asyncio.create_task(
|
||||
get_month_data(month)) for month in months]
|
||||
month_tasks = [asyncio.create_task(get_month_data(month)) for month in months]
|
||||
|
||||
await asyncio.gather(transport.run(), show_images(), run_tk())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Simple Daily Bot Sample")
|
||||
parser.add_argument(
|
||||
"-u",
|
||||
"--url",
|
||||
type=str,
|
||||
required=True,
|
||||
help="URL of the Daily room to join")
|
||||
"-u", "--url", type=str, required=True, help="URL of the Daily room to join"
|
||||
)
|
||||
|
||||
args, unknown = parser.parse_known_args()
|
||||
|
||||
|
||||
@@ -1,50 +1,41 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import os
|
||||
from dailyai.pipeline.frames import LLMMessagesQueueFrame
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService
|
||||
from dailyai.services.azure_ai_services import AzureLLMService, AzureTTSService
|
||||
from dailyai.services.ai_services import FrameLogger
|
||||
from dailyai.pipeline.aggregators import (
|
||||
LLMAssistantContextAggregator,
|
||||
LLMUserContextAggregator,
|
||||
)
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
from dailyai.pipeline.aggregators import LLMAssistantContextAggregator, LLMUserContextAggregator
|
||||
from examples.foundational.support.runner import configure
|
||||
|
||||
|
||||
async def main(room_url: str, token):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
token,
|
||||
"Respond bot",
|
||||
duration_minutes=5,
|
||||
start_transcription=True,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=False,
|
||||
vad_enabled=True,
|
||||
)
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
token,
|
||||
"Respond bot",
|
||||
duration_minutes=5,
|
||||
start_transcription=True,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=False,
|
||||
vad_enabled=True
|
||||
)
|
||||
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"),
|
||||
)
|
||||
llm = AzureLLMService(
|
||||
api_key=os.getenv("AZURE_CHATGPT_API_KEY"),
|
||||
endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"),
|
||||
model=os.getenv("AZURE_CHATGPT_MODEL"))
|
||||
tts = AzureTTSService(
|
||||
api_key=os.getenv("AZURE_SPEECH_API_KEY"),
|
||||
region=os.getenv("AZURE_SPEECH_REGION"))
|
||||
fl = FrameLogger("Inner")
|
||||
fl2 = FrameLogger("Outer")
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await tts.say("Hi, I'm listening!", transport.send_queue)
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
fl = FrameLogger("Inner")
|
||||
fl2 = FrameLogger("Outer")
|
||||
async def handle_transcriptions():
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
@@ -52,32 +43,23 @@ async def main(room_url: str, token):
|
||||
},
|
||||
]
|
||||
|
||||
tma_in = LLMUserContextAggregator(
|
||||
messages, transport._my_participant_id)
|
||||
tma_out = LLMAssistantContextAggregator(
|
||||
messages, transport._my_participant_id
|
||||
)
|
||||
tma_in = LLMUserContextAggregator(messages, transport._my_participant_id)
|
||||
tma_out = LLMAssistantContextAggregator(messages, transport._my_participant_id)
|
||||
pipeline = Pipeline(
|
||||
processors=[
|
||||
fl,
|
||||
tma_in,
|
||||
llm,
|
||||
fl2,
|
||||
tts,
|
||||
tma_out,
|
||||
tts
|
||||
],
|
||||
)
|
||||
await transport.run_uninterruptible_pipeline(pipeline)
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
# Kick off the conversation.
|
||||
messages.append(
|
||||
{"role": "system", "content": "Please introduce yourself to the user."})
|
||||
await pipeline.queue_frames([LLMMessagesQueueFrame(messages)])
|
||||
|
||||
transport.transcription_settings["extra"]["endpointing"] = True
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
await transport.run(pipeline)
|
||||
transport.transcription_settings["extra"]["endpointing"] = True
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
await asyncio.gather(transport.run(), handle_transcriptions())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,29 +1,22 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
from typing import AsyncGenerator
|
||||
import aiohttp
|
||||
import requests
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from dailyai.pipeline.frames import ImageFrame, Frame
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.ai_services import AIService
|
||||
from dailyai.pipeline.aggregators import (
|
||||
LLMAssistantContextAggregator,
|
||||
LLMUserContextAggregator,
|
||||
)
|
||||
from dailyai.services.open_ai_services import OpenAILLMService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.fal_ai_services import FalImageGenService
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.azure_ai_services import AzureLLMService, AzureTTSService
|
||||
from dailyai.services.ai_services import AIService
|
||||
from dailyai.pipeline.aggregators import LLMAssistantContextAggregator, LLMUserContextAggregator
|
||||
from dailyai.services.fal_ai_services import FalImageGenService
|
||||
|
||||
from examples.foundational.support.runner import configure
|
||||
|
||||
|
||||
class ImageSyncAggregator(AIService):
|
||||
@@ -54,22 +47,18 @@ async def main(room_url: str, token):
|
||||
transport._mic_enabled = True
|
||||
transport._mic_sample_rate = 16000
|
||||
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"),
|
||||
)
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
|
||||
llm = AzureLLMService(
|
||||
api_key=os.getenv("AZURE_CHATGPT_API_KEY"),
|
||||
endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"),
|
||||
model=os.getenv("AZURE_CHATGPT_MODEL"))
|
||||
tts = AzureTTSService(
|
||||
api_key=os.getenv("AZURE_SPEECH_API_KEY"),
|
||||
region=os.getenv("AZURE_SPEECH_REGION"))
|
||||
img = FalImageGenService(
|
||||
image_size="1024x1024",
|
||||
aiohttp_session=session,
|
||||
key_id=os.getenv("FAL_KEY_ID"),
|
||||
key_secret=os.getenv("FAL_KEY_SECRET"),
|
||||
)
|
||||
key_secret=os.getenv("FAL_KEY_SECRET"))
|
||||
|
||||
async def get_images():
|
||||
get_speaking_task = asyncio.create_task(
|
||||
@@ -91,26 +80,30 @@ async def main(room_url: str, token):
|
||||
|
||||
async def handle_transcriptions():
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio. Respond to what the user said in a creative and helpful way.",
|
||||
},
|
||||
{"role": "system", "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio. Respond to what the user said in a creative and helpful way."},
|
||||
]
|
||||
|
||||
tma_in = LLMUserContextAggregator(
|
||||
messages, transport._my_participant_id)
|
||||
messages, transport._my_participant_id
|
||||
)
|
||||
tma_out = LLMAssistantContextAggregator(
|
||||
messages, transport._my_participant_id
|
||||
)
|
||||
image_sync_aggregator = ImageSyncAggregator(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__), "assets", "speaking.png"), os.path.join(
|
||||
os.path.dirname(__file__), "assets", "waiting.png"), )
|
||||
os.path.join(os.path.dirname(__file__), "assets", "speaking.png"),
|
||||
os.path.join(os.path.dirname(__file__), "assets", "waiting.png"),
|
||||
)
|
||||
await tts.run_to_queue(
|
||||
transport.send_queue,
|
||||
image_sync_aggregator.run(
|
||||
tma_out.run(llm.run(tma_in.run(transport.get_receive_frames())))
|
||||
),
|
||||
tma_out.run(
|
||||
llm.run(
|
||||
tma_in.run(
|
||||
transport.get_receive_frames()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import os
|
||||
from dailyai.pipeline.aggregators import (
|
||||
LLMAssistantContextAggregator,
|
||||
LLMResponseAggregator,
|
||||
LLMUserContextAggregator,
|
||||
UserResponseAggregator,
|
||||
)
|
||||
from dailyai.pipeline.aggregators import LLMAssistantContextAggregator, LLMResponseAggregator, LLMUserContextAggregator, UserResponseAggregator
|
||||
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.services.ai_services import FrameLogger
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from examples.support.runner import configure
|
||||
from dailyai.services.azure_ai_services import AzureLLMService, AzureTTSService
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
from support.runner import configure
|
||||
|
||||
|
||||
async def main(room_url: str, token):
|
||||
@@ -35,34 +25,33 @@ async def main(room_url: str, token):
|
||||
vad_enabled=True,
|
||||
)
|
||||
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"),
|
||||
)
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
llm = AzureLLMService(
|
||||
api_key=os.getenv("AZURE_CHATGPT_API_KEY"),
|
||||
endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"),
|
||||
model=os.getenv("AZURE_CHATGPT_MODEL"))
|
||||
tts = AzureTTSService(
|
||||
api_key=os.getenv("AZURE_SPEECH_API_KEY"),
|
||||
region=os.getenv("AZURE_SPEECH_REGION"))
|
||||
|
||||
pipeline = Pipeline([FrameLogger(), llm, FrameLogger(), tts])
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await transport.say("Hi, I'm listening!", tts)
|
||||
await tts.say("Hi, I'm listening!", transport.send_queue)
|
||||
|
||||
async def run_conversation():
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio. Respond to what the user said in a creative and helpful way.",
|
||||
},
|
||||
{"role": "system", "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio. Respond to what the user said in a creative and helpful way."},
|
||||
]
|
||||
|
||||
await transport.run_interruptible_pipeline(
|
||||
pipeline,
|
||||
post_processor=LLMResponseAggregator(messages),
|
||||
pre_processor=UserResponseAggregator(messages),
|
||||
post_processor=LLMResponseAggregator(
|
||||
messages
|
||||
),
|
||||
pre_processor=UserResponseAggregator(
|
||||
messages
|
||||
),
|
||||
)
|
||||
|
||||
transport.transcription_settings["extra"]["punctuate"] = False
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
from typing import Tuple
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from dailyai.pipeline.aggregators import SentenceAggregator
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.azure_ai_services import AzureLLMService, AzureTTSService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.fal_ai_services import FalImageGenService
|
||||
from dailyai.pipeline.frames import AudioFrame, EndFrame, ImageFrame, LLMMessagesQueueFrame, TextFrame
|
||||
from examples.support.runner import configure
|
||||
from dailyai.pipeline.frames import AudioFrame, ImageFrame
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
from examples.foundational.support.runner import configure
|
||||
|
||||
|
||||
async def main(room_url: str):
|
||||
@@ -29,83 +22,62 @@ async def main(room_url: str):
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=True,
|
||||
camera_width=1024,
|
||||
camera_height=1024,
|
||||
camera_height=1024
|
||||
)
|
||||
|
||||
llm = AzureLLMService(
|
||||
api_key=os.getenv("AZURE_CHATGPT_API_KEY"),
|
||||
endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"),
|
||||
model=os.getenv("AZURE_CHATGPT_MODEL"),
|
||||
)
|
||||
model=os.getenv("AZURE_CHATGPT_MODEL"))
|
||||
tts1 = AzureTTSService(
|
||||
api_key=os.getenv("AZURE_SPEECH_API_KEY"),
|
||||
region=os.getenv("AZURE_SPEECH_REGION"),
|
||||
)
|
||||
region=os.getenv("AZURE_SPEECH_REGION"))
|
||||
tts2 = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id="jBpfuIE2acCO8z3wKNLl",
|
||||
)
|
||||
voice_id="jBpfuIE2acCO8z3wKNLl")
|
||||
dalle = FalImageGenService(
|
||||
image_size="1024x1024",
|
||||
aiohttp_session=session,
|
||||
key_id=os.getenv("FAL_KEY_ID"),
|
||||
key_secret=os.getenv("FAL_KEY_SECRET"),
|
||||
)
|
||||
key_secret=os.getenv("FAL_KEY_SECRET"))
|
||||
|
||||
bot1_messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a stern librarian. You strongly believe that a hot dog is a sandwich. Start by stating this fact in a few sentences, then be prepared to debate this with the user. You shouldn't ever compromise on the fundamental truth that a hot dog is a sandwich. Your responses should only be a few sentences long.",
|
||||
},
|
||||
{"role": "system", "content": "You are a stern librarian. You strongly believe that a hot dog is a sandwich. Start by stating this fact in a few sentences, then be prepared to debate this with the user. You shouldn't ever compromise on the fundamental truth that a hot dog is a sandwich. Your responses should only be a few sentences long."},
|
||||
]
|
||||
bot2_messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a silly cat, and you strongly believe that a hot dog is not a sandwich. Debate this with the user, only responding with a few sentences. Don't ever accept that a hot dog is a sandwich.",
|
||||
},
|
||||
"content": "You are a silly cat, and you strongly believe that a hot dog is not a sandwich. Debate this with the user, only responding with a few sentences. Don't ever accept that a hot dog is a sandwich."},
|
||||
]
|
||||
|
||||
async def get_text_and_audio(messages) -> Tuple[str, bytearray]:
|
||||
"""This function streams text from the LLM and uses the TTS service to convert
|
||||
that text to speech as it's received. """
|
||||
source_queue = asyncio.Queue()
|
||||
sink_queue = asyncio.Queue()
|
||||
sentence_aggregator = SentenceAggregator()
|
||||
pipeline = Pipeline(
|
||||
[llm, sentence_aggregator, tts1], source_queue, sink_queue
|
||||
)
|
||||
|
||||
await source_queue.put(LLMMessagesQueueFrame(messages))
|
||||
await source_queue.put(EndFrame())
|
||||
await pipeline.run_pipeline()
|
||||
|
||||
message = ""
|
||||
all_audio = bytearray()
|
||||
while sink_queue.qsize():
|
||||
frame = sink_queue.get_nowait()
|
||||
if isinstance(frame, TextFrame):
|
||||
message += frame.text
|
||||
elif isinstance(frame, AudioFrame):
|
||||
all_audio.extend(frame.data)
|
||||
|
||||
return (message, all_audio)
|
||||
|
||||
async def get_bot1_statement():
|
||||
message, audio = await get_text_and_audio(bot1_messages)
|
||||
# Run the LLMs synchronously for the back-and-forth
|
||||
bot1_msg = await llm.run_llm(bot1_messages)
|
||||
print(f"bot1_msg: {bot1_msg}")
|
||||
if bot1_msg:
|
||||
bot1_messages.append({"role": "assistant", "content": bot1_msg})
|
||||
bot2_messages.append({"role": "user", "content": bot1_msg})
|
||||
|
||||
bot1_messages.append({"role": "assistant", "content": message})
|
||||
bot2_messages.append({"role": "user", "content": message})
|
||||
all_audio = bytearray()
|
||||
async for audio in tts1.run_tts(bot1_msg):
|
||||
all_audio.extend(audio)
|
||||
|
||||
return audio
|
||||
return all_audio
|
||||
|
||||
async def get_bot2_statement():
|
||||
message, audio = await get_text_and_audio(bot2_messages)
|
||||
# Run the LLMs synchronously for the back-and-forth
|
||||
bot2_msg = await llm.run_llm(bot2_messages)
|
||||
print(f"bot2_msg: {bot2_msg}")
|
||||
if bot2_msg:
|
||||
bot2_messages.append({"role": "assistant", "content": bot2_msg})
|
||||
bot1_messages.append({"role": "user", "content": bot2_msg})
|
||||
|
||||
bot2_messages.append({"role": "assistant", "content": message})
|
||||
bot1_messages.append({"role": "user", "content": message})
|
||||
all_audio = bytearray()
|
||||
async for audio in tts2.run_tts(bot2_msg):
|
||||
all_audio.extend(audio)
|
||||
|
||||
return audio
|
||||
return all_audio
|
||||
|
||||
async def argue():
|
||||
for i in range(100):
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService
|
||||
from dailyai.services.azure_ai_services import AzureLLMService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.pipeline.aggregators import (
|
||||
LLMUserContextAggregator,
|
||||
LLMAssistantContextAggregator,
|
||||
)
|
||||
from dailyai.pipeline.aggregators import LLMUserContextAggregator, LLMAssistantContextAggregator
|
||||
from dailyai.pipeline.frames import (
|
||||
Frame,
|
||||
TextFrame,
|
||||
@@ -21,21 +18,19 @@ from dailyai.pipeline.frames import (
|
||||
TranscriptionQueueFrame,
|
||||
)
|
||||
from dailyai.services.ai_services import AIService
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
from examples.foundational.support.runner import configure
|
||||
|
||||
|
||||
sprites = {}
|
||||
image_files = [
|
||||
"sc-default.png",
|
||||
"sc-talk.png",
|
||||
"sc-listen-1.png",
|
||||
"sc-think-1.png",
|
||||
"sc-think-2.png",
|
||||
"sc-think-3.png",
|
||||
"sc-think-4.png",
|
||||
'sc-default.png',
|
||||
'sc-talk.png',
|
||||
'sc-listen-1.png',
|
||||
'sc-think-1.png',
|
||||
'sc-think-2.png',
|
||||
'sc-think-3.png',
|
||||
'sc-think-4.png'
|
||||
]
|
||||
|
||||
script_dir = os.path.dirname(__file__)
|
||||
@@ -52,18 +47,16 @@ for file in image_files:
|
||||
# When the bot isn't talking, show a static image of the cat listening
|
||||
quiet_frame = ImageFrame("", sprites["sc-listen-1.png"])
|
||||
# When the bot is talking, build an animation from two sprites
|
||||
talking_list = [sprites["sc-default.png"], sprites["sc-talk.png"]]
|
||||
talking_list = [sprites['sc-default.png'], sprites['sc-talk.png']]
|
||||
talking = [random.choice(talking_list) for x in range(30)]
|
||||
talking_frame = SpriteFrame(images=talking)
|
||||
|
||||
# TODO: Support "thinking" as soon as we get a valid transcript, while LLM
|
||||
# is processing
|
||||
# TODO: Support "thinking" as soon as we get a valid transcript, while LLM is processing
|
||||
thinking_list = [
|
||||
sprites["sc-think-1.png"],
|
||||
sprites["sc-think-2.png"],
|
||||
sprites["sc-think-3.png"],
|
||||
sprites["sc-think-4.png"],
|
||||
]
|
||||
sprites['sc-think-1.png'],
|
||||
sprites['sc-think-2.png'],
|
||||
sprites['sc-think-3.png'],
|
||||
sprites['sc-think-4.png']]
|
||||
thinking_frame = SpriteFrame(images=thinking_list)
|
||||
|
||||
|
||||
@@ -122,7 +115,7 @@ async def main(room_url: str, token):
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=True,
|
||||
camera_width=720,
|
||||
camera_height=1280,
|
||||
camera_height=1280
|
||||
)
|
||||
transport._mic_enabled = True
|
||||
transport._mic_sample_rate = 16000
|
||||
@@ -130,34 +123,28 @@ async def main(room_url: str, token):
|
||||
transport._camera_width = 720
|
||||
transport._camera_height = 1280
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
|
||||
llm = AzureLLMService(
|
||||
api_key=os.getenv("AZURE_CHATGPT_API_KEY"),
|
||||
endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"),
|
||||
model=os.getenv("AZURE_CHATGPT_MODEL"))
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id="jBpfuIE2acCO8z3wKNLl",
|
||||
)
|
||||
voice_id="jBpfuIE2acCO8z3wKNLl")
|
||||
isa = ImageSyncAggregator()
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await tts.say(
|
||||
"Hi! If you want to talk to me, just say 'hey Santa Cat'.",
|
||||
transport.send_queue,
|
||||
)
|
||||
await tts.say("Hi! If you want to talk to me, just say 'hey Santa Cat'.", transport.send_queue)
|
||||
|
||||
async def handle_transcriptions():
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are Santa Cat, a cat that lives in Santa's workshop at the North Pole. You should be clever, and a bit sarcastic. You should also tell jokes every once in a while. Your responses should only be a few sentences long.",
|
||||
},
|
||||
{"role": "system", "content": "You are Santa Cat, a cat that lives in Santa's workshop at the North Pole. You should be clever, and a bit sarcastic. You should also tell jokes every once in a while. Your responses should only be a few sentences long."},
|
||||
]
|
||||
|
||||
tma_in = LLMUserContextAggregator(
|
||||
messages, transport._my_participant_id)
|
||||
messages, transport._my_participant_id
|
||||
)
|
||||
tma_out = LLMAssistantContextAggregator(
|
||||
messages, transport._my_participant_id
|
||||
)
|
||||
@@ -168,10 +155,16 @@ async def main(room_url: str, token):
|
||||
isa.run(
|
||||
tma_out.run(
|
||||
llm.run(
|
||||
tma_in.run(ncf.run(tf.run(transport.get_receive_frames())))
|
||||
tma_in.run(
|
||||
ncf.run(
|
||||
tf.run(
|
||||
transport.get_receive_frames()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
async def starting_image():
|
||||
|
||||
@@ -5,30 +5,24 @@ import os
|
||||
import wave
|
||||
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService
|
||||
from dailyai.services.azure_ai_services import AzureLLMService, AzureTTSService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.pipeline.aggregators import (
|
||||
LLMContextAggregator,
|
||||
LLMUserContextAggregator,
|
||||
LLMAssistantContextAggregator,
|
||||
)
|
||||
from dailyai.pipeline.aggregators import LLMContextAggregator, LLMUserContextAggregator, LLMAssistantContextAggregator
|
||||
from dailyai.services.ai_services import AIService, FrameLogger
|
||||
from dailyai.pipeline.frames import (
|
||||
Frame,
|
||||
AudioFrame,
|
||||
LLMResponseEndFrame,
|
||||
LLMMessagesQueueFrame,
|
||||
)
|
||||
from dailyai.pipeline.frames import Frame, AudioFrame, LLMResponseEndFrame, LLMMessagesQueueFrame
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from examples.support.runner import configure
|
||||
from examples.foundational.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s") # or whatever
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
sounds = {}
|
||||
sound_files = ["ding1.wav", "ding2.wav"]
|
||||
sound_files = [
|
||||
'ding1.wav',
|
||||
'ding2.wav'
|
||||
]
|
||||
|
||||
script_dir = os.path.dirname(__file__)
|
||||
|
||||
@@ -77,18 +71,17 @@ async def main(room_url: str, token):
|
||||
duration_minutes=5,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=False,
|
||||
camera_enabled=False
|
||||
)
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
|
||||
llm = AzureLLMService(
|
||||
api_key=os.getenv("AZURE_CHATGPT_API_KEY"),
|
||||
endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"),
|
||||
model=os.getenv("AZURE_CHATGPT_MODEL"))
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id="ErXwobaYiN019PkySvjV",
|
||||
)
|
||||
voice_id="ErXwobaYiN019PkySvjV")
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
@@ -97,14 +90,12 @@ async def main(room_url: str, token):
|
||||
|
||||
async def handle_transcriptions():
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio. Respond to what the user said in a creative and helpful way.",
|
||||
},
|
||||
{"role": "system", "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio. Respond to what the user said in a creative and helpful way."},
|
||||
]
|
||||
|
||||
tma_in = LLMUserContextAggregator(
|
||||
messages, transport._my_participant_id)
|
||||
messages, transport._my_participant_id
|
||||
)
|
||||
tma_out = LLMAssistantContextAggregator(
|
||||
messages, transport._my_participant_id
|
||||
)
|
||||
@@ -120,13 +111,15 @@ async def main(room_url: str, token):
|
||||
llm.run(
|
||||
fl2.run(
|
||||
in_sound.run(
|
||||
tma_in.run(transport.get_receive_frames())
|
||||
tma_in.run(
|
||||
transport.get_receive_frames()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import os
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from dailyai.pipeline.frames import Frame, LLMMessagesQueueFrame, RequestVideoImageFrame, LLMResponseEndFrame
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.pipeline.frame_processor import FrameProcessor
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService, OpenAIVisionService
|
||||
from dailyai.services.deepgram_ai_services import DeepgramTTSService
|
||||
from dailyai.services.ai_services import FrameLogger
|
||||
from dailyai.pipeline.aggregators import (
|
||||
LLMAssistantContextAggregator,
|
||||
LLMUserContextAggregator,
|
||||
)
|
||||
from dailyai.pipeline.frames import VideoImageFrame, VisionFrame
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class VideoImageFrameProcessor(FrameProcessor):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, VideoImageFrame):
|
||||
yield VisionFrame("Describe the image in one sentence.", frame.image)
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class ImageRefresher(FrameProcessor):
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, LLMResponseEndFrame):
|
||||
yield RequestVideoImageFrame(participantId=None)
|
||||
yield frame
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
async def main(room_url: str, token):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
token,
|
||||
"Respond bot",
|
||||
duration_minutes=5,
|
||||
start_transcription=True,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=False,
|
||||
vad_enabled=True,
|
||||
receive_video=True,
|
||||
receive_video_fps=0
|
||||
)
|
||||
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"),
|
||||
)
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
|
||||
vs = OpenAIVisionService(api_key=os.getenv("OPENAI_CHATGPT_API_KEY"))
|
||||
vifp = VideoImageFrameProcessor()
|
||||
ir = ImageRefresher()
|
||||
pipeline = Pipeline(
|
||||
processors=[
|
||||
vifp,
|
||||
vs,
|
||||
llm,
|
||||
tts,
|
||||
ir,
|
||||
],
|
||||
)
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await pipeline.queue_frames([RequestVideoImageFrame(participantId=None)])
|
||||
|
||||
transport.transcription_settings["extra"]["endpointing"] = True
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
await transport.run(pipeline)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
(url, token) = configure()
|
||||
asyncio.run(main(url, token))
|
||||
@@ -1,13 +1,9 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.whisper_ai_services import WhisperSTTService
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
from examples.foundational.support.runner import configure
|
||||
|
||||
|
||||
async def main(room_url: str):
|
||||
@@ -18,7 +14,7 @@ async def main(room_url: str):
|
||||
start_transcription=True,
|
||||
mic_enabled=False,
|
||||
camera_enabled=False,
|
||||
speaker_enabled=True,
|
||||
speaker_enabled=True
|
||||
)
|
||||
|
||||
stt = WhisperSTTService()
|
||||
@@ -32,9 +28,9 @@ async def main(room_url: str):
|
||||
|
||||
async def handle_speaker():
|
||||
await stt.run_to_queue(
|
||||
transcription_output_queue, transport.get_receive_frames()
|
||||
transcription_output_queue,
|
||||
transport.get_receive_frames()
|
||||
)
|
||||
|
||||
await asyncio.gather(transport.run(), handle_speaker(), handle_transcription())
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import wave
|
||||
from dailyai.pipeline.frames import EndFrame, TranscriptionQueueFrame
|
||||
|
||||
from dailyai.services.local_transport_service import LocalTransportService
|
||||
from dailyai.services.whisper_ai_services import WhisperSTTService
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
async def main(room_url: str):
|
||||
global transport
|
||||
@@ -22,7 +17,7 @@ async def main(room_url: str):
|
||||
camera_enabled=False,
|
||||
speaker_enabled=True,
|
||||
duration_minutes=meeting_duration_minutes,
|
||||
start_transcription=True,
|
||||
start_transcription=True
|
||||
)
|
||||
stt = WhisperSTTService()
|
||||
transcription_output_queue = asyncio.Queue()
|
||||
@@ -57,11 +52,8 @@ async def main(room_url: str):
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Simple Daily Bot Sample")
|
||||
parser.add_argument(
|
||||
"-u",
|
||||
"--url",
|
||||
type=str,
|
||||
required=True,
|
||||
help="URL of the Daily room to join")
|
||||
"-u", "--url", type=str, required=True, help="URL of the Daily room to join"
|
||||
)
|
||||
|
||||
args, unknown = parser.parse_known_args()
|
||||
asyncio.run(main(args.url))
|
||||
|
||||
376
src/examples/foundational/14-patient-intake.py
Normal file
@@ -0,0 +1,376 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import os
|
||||
import re
|
||||
import wave
|
||||
from typing import AsyncGenerator
|
||||
from PIL import Image
|
||||
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.azure_ai_services import AzureLLMService, AzureTTSService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService
|
||||
from dailyai.services.deepgram_ai_services import DeepgramTTSService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.pipeline.aggregators import LLMAssistantContextAggregator, LLMContextAggregator, LLMUserContextAggregator, UserResponseAggregator, LLMResponseAggregator
|
||||
from support.runner import configure
|
||||
from dailyai.pipeline.frames import LLMMessagesQueueFrame, TranscriptionQueueFrame, Frame, TextFrame, LLMFunctionCallFrame, LLMResponseEndFrame, StartFrame, AudioFrame, SpriteFrame, ImageFrame
|
||||
from dailyai.services.ai_services import FrameLogger, AIService
|
||||
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
sounds = {}
|
||||
sound_files = [
|
||||
'clack-short.wav',
|
||||
'clack.wav',
|
||||
'clack-short-quiet.wav'
|
||||
]
|
||||
|
||||
script_dir = os.path.dirname(__file__)
|
||||
|
||||
for file in sound_files:
|
||||
# Build the full path to the image file
|
||||
full_path = os.path.join(script_dir, "assets", file)
|
||||
# Get the filename without the extension to use as the dictionary key
|
||||
filename = os.path.splitext(os.path.basename(full_path))[0]
|
||||
# Open the image and convert it to bytes
|
||||
with wave.open(full_path) as audio_file:
|
||||
sounds[file] = audio_file.readframes(-1)
|
||||
|
||||
|
||||
steps = [
|
||||
{
|
||||
"prompt": "Start by introducing yourself. Then, ask the user to confirm their identity by telling you their birthday, including the year. When they answer with their birthday, call the verify_birthday function.",
|
||||
"run_async": False,
|
||||
"failed": "The user provided an incorrect birthday. Ask them for their birthday again. When they answer, call the verify_birthday function.", "tools": [{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "verify_birthday",
|
||||
"description": "Use this function to verify the user has provided their correct birthday.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"birthday": {
|
||||
"type": "string",
|
||||
"description": "The user's birthdate, including the year. The user can provide it in any format, but convert it to YYYY-MM-DD format to call this function."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}]},
|
||||
{
|
||||
"prompt": "Next, thank the user for confirming their identity, then ask the user to list their current prescriptions. Each prescription needs to have a medication name and a dosage. Do not call the list_prescriptions function with any unknown dosages.",
|
||||
"run_async": True,
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_prescriptions",
|
||||
"description": "Once the user has provided a list of their prescription medications, call this function.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prescriptions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"medication": {
|
||||
"type": "string",
|
||||
"description": "The medication's name"
|
||||
},
|
||||
"dosage": {
|
||||
"type": "string",
|
||||
"description": "The prescription's dosage"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
"prompt": "Next, ask the user if they have any allergies. Once they have listed their allergies or confirmed they don't have any, call the list_allergies function.",
|
||||
"run_async": True,
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_allergies",
|
||||
"description": "Once the user has provided a list of their allergies, call this function.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allergies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "What the user is allergic to"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"prompt": "Now ask the user if they have any medical conditions the doctor should know about. Once they've answered the question, call the list_conditions function.",
|
||||
"run_async": True,
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_conditions",
|
||||
"description": "Once the user has provided a list of their medical conditions, call this function.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The user's medical condition"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"prompt": "Finally, ask the user the reason for their doctor visit today. Once they answer, call the list_visit_reasons function.",
|
||||
"run_async": True,
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_visit_reasons",
|
||||
"description": "Once the user has provided a list of the reasons they are visiting a doctor today, call this function.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"visit_reasons": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The user's reason for visiting the doctor"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{"prompt": "Now, thank the user and end the conversation.",
|
||||
"run_async": True, "tools": []},
|
||||
{"prompt": "", "run_async": True, "tools": []}
|
||||
]
|
||||
current_step = 0
|
||||
|
||||
|
||||
class TranscriptFilter(AIService):
|
||||
def __init__(self, bot_participant_id=None):
|
||||
super().__init__()
|
||||
self.bot_participant_id = bot_participant_id
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, TranscriptionQueueFrame):
|
||||
if frame.participantId != self.bot_participant_id:
|
||||
yield frame
|
||||
|
||||
|
||||
class ChecklistProcessor(AIService):
|
||||
def __init__(self, messages, llm, tools, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._messages = messages
|
||||
self._llm = llm
|
||||
self._tools = tools
|
||||
self._function_name = ""
|
||||
self._arguments = ""
|
||||
self._id = "You are Jessica, an agent for a company called Tri-County Health Services. Your job is to collect important information from the user before their doctor visit. You're talking to Chad Bailey. You should address the user by their first name and be polite and professional. You're not a medical professional, so you shouldn't provide any advice. Keep your responses short. Your job is to collect information to give to a doctor. Don't make assumptions about what values to plug into functions. Ask for clarification if a user response is ambiguous."
|
||||
self._acks = ["One sec.", "Let me confirm that.", "Thanks.", "OK."]
|
||||
|
||||
messages.append(
|
||||
{"role": "system", "content": f"{self._id} {steps[0]['prompt']}"})
|
||||
|
||||
def verify_birthday(self, args):
|
||||
return args['birthday'] == "1983-01-01"
|
||||
|
||||
def list_prescriptions(self, args):
|
||||
# print(f"--- Prescriptions: {args['prescriptions']}\n")
|
||||
pass
|
||||
|
||||
def list_allergies(self, args):
|
||||
# print(f"--- Allergies: {args['allergies']}\n")
|
||||
pass
|
||||
|
||||
def list_conditions(self, args):
|
||||
# print(f"--- Medical Conditions: {args['conditions']}")
|
||||
pass
|
||||
|
||||
def list_visit_reasons(self, args):
|
||||
# print(f"Visit Reasons: {args['visit_reasons']}")
|
||||
pass
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
global current_step
|
||||
this_step = steps[current_step]
|
||||
# TODO-CB: forcing a global here :/
|
||||
self._tools.clear()
|
||||
self._tools.extend(this_step['tools'])
|
||||
if isinstance(frame, LLMFunctionCallFrame) and frame.function_name:
|
||||
print(f"... Preparing function call: {frame.function_name}")
|
||||
self._function_name = frame.function_name
|
||||
if this_step['run_async']:
|
||||
# Get the LLM talking about the next step before getting the rest
|
||||
# of the function call completion
|
||||
current_step += 1
|
||||
# yield TextFrame(f"We should move on to Step {current_step}.")
|
||||
self._messages.append({
|
||||
"role": "system", "content": steps[current_step]['prompt']})
|
||||
# yield LLMMessagesQueueFrame(self._messages)
|
||||
yield LLMMessagesQueueFrame(self._messages)
|
||||
async for frame in llm.process_frame(LLMMessagesQueueFrame(self._messages), tool_choice="none"):
|
||||
yield frame
|
||||
else:
|
||||
# Insert a quick response while we run the function
|
||||
# yield AudioFrame(sounds["clack-short-quiet.wav"])
|
||||
pass
|
||||
elif isinstance(frame, LLMFunctionCallFrame) and frame.arguments:
|
||||
self._arguments += frame.arguments
|
||||
elif isinstance(frame, LLMResponseEndFrame):
|
||||
|
||||
if self._function_name and self._arguments:
|
||||
print(
|
||||
f"--> Calling function: {self._function_name} with arguments:")
|
||||
pretty_json = re.sub("\n", "\n ", json.dumps(
|
||||
json.loads(self._arguments), indent=2))
|
||||
print(f"--> {pretty_json}\n")
|
||||
fn = getattr(self, self._function_name)
|
||||
result = fn(json.loads(self._arguments))
|
||||
self._function_name = ""
|
||||
self._arguments = ""
|
||||
if not this_step['run_async']:
|
||||
if result:
|
||||
current_step += 1
|
||||
# yield TextFrame(f"We should move on to Step {current_step}.")
|
||||
self._messages.append({
|
||||
"role": "system", "content": steps[current_step]['prompt']})
|
||||
# yield LLMMessagesQueueFrame(self._messages)
|
||||
yield LLMMessagesQueueFrame(self._messages)
|
||||
async for frame in llm.process_frame(LLMMessagesQueueFrame(self._messages), tool_choice="none"):
|
||||
yield frame
|
||||
else:
|
||||
self._messages.append({
|
||||
"role": "system", "content": this_step['failed']})
|
||||
# yield LLMMessagesQueueFrame(self._messages)
|
||||
yield LLMMessagesQueueFrame(self._messages)
|
||||
async for frame in llm.process_frame(LLMMessagesQueueFrame(self._messages), tool_choice="none"):
|
||||
yield frame
|
||||
print(f"<-- Verify result: {result}\n")
|
||||
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
async def main(room_url: str, token):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
global transport
|
||||
global llm
|
||||
global tts
|
||||
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
token,
|
||||
"Intake Bot",
|
||||
5,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=False,
|
||||
start_transcription=True,
|
||||
vad_enabled=True
|
||||
)
|
||||
# TODO-CB: Go back to vad_enabled
|
||||
|
||||
messages = []
|
||||
tools = []
|
||||
|
||||
# llm = AzureLLMService(api_key=os.getenv("AZURE_CHATGPT_API_KEY"), endpoint=os.getenv(
|
||||
# "AZURE_CHATGPT_ENDPOINT"), model=os.getenv("AZURE_CHATGPT_MODEL"))
|
||||
llm = OpenAILLMService(api_key=os.getenv(
|
||||
"OPENAI_CHATGPT_API_KEY"), model="gpt-4-1106-preview", tools=tools) # gpt-4-1106-preview
|
||||
# tts = AzureTTSService(api_key=os.getenv(
|
||||
# "AZURE_SPEECH_API_KEY"), region=os.getenv("AZURE_SPEECH_REGION"))
|
||||
tts = ElevenLabsTTSService(aiohttp_session=session, api_key=os.getenv(
|
||||
"ELEVENLABS_API_KEY"), voice_id="XrExE9yKIg1WjnnlVkGX") # matilda
|
||||
# tts = DeepgramTTSService(aiohttp_session=session, api_key=os.getenv(
|
||||
# "DEEPGRAM_API_KEY"), voice="aura-asteria-en")
|
||||
|
||||
# lca = LLMContextAggregator(
|
||||
# messages=messages, bot_participant_id=transport._my_participant_id)
|
||||
checklist = ChecklistProcessor(messages, llm, tools)
|
||||
fl = FrameLogger("FRAME LOGGER 1:")
|
||||
fl2 = FrameLogger("FRAME LOGGER 2:")
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
fl = FrameLogger("first other participant")
|
||||
# TODO-CB: Make sure this message gets into the context somehow
|
||||
await tts.run_to_queue(
|
||||
transport.send_queue,
|
||||
llm.run([LLMMessagesQueueFrame(messages)]),
|
||||
|
||||
)
|
||||
|
||||
async def handle_intake():
|
||||
pipeline = Pipeline(
|
||||
processors=[
|
||||
fl,
|
||||
llm,
|
||||
fl2,
|
||||
checklist,
|
||||
tts
|
||||
]
|
||||
)
|
||||
await transport.run_interruptible_pipeline(pipeline,
|
||||
post_processor=LLMResponseAggregator(
|
||||
messages
|
||||
),
|
||||
pre_processor=UserResponseAggregator(messages)
|
||||
)
|
||||
|
||||
|
||||
transport.transcription_settings["extra"]["endpointing"] = True
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
try:
|
||||
await asyncio.gather(transport.run(), handle_intake())
|
||||
except (asyncio.CancelledError, KeyboardInterrupt):
|
||||
print('whoops')
|
||||
transport.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
(url, token) = configure()
|
||||
asyncio.run(main(url, token))
|
||||
@@ -11,11 +11,8 @@ load_dotenv()
|
||||
def configure():
|
||||
parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample")
|
||||
parser.add_argument(
|
||||
"-u",
|
||||
"--url",
|
||||
type=str,
|
||||
required=False,
|
||||
help="URL of the Daily room to join")
|
||||
"-u", "--url", type=str, required=False, help="URL of the Daily room to join"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-k",
|
||||
"--apikey",
|
||||
@@ -36,25 +33,20 @@ def configure():
|
||||
if not key:
|
||||
raise Exception("No Daily API key specified. use the -k/--apikey option from the command line, or set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers.")
|
||||
|
||||
# Create a meeting token for the given room with an expiration 1 hour in
|
||||
# the future.
|
||||
# Create a meeting token for the given room with an expiration 1 hour in the future.
|
||||
room_name: str = urllib.parse.urlparse(url).path[1:]
|
||||
expiration: float = time.time() + 60 * 60
|
||||
|
||||
res: requests.Response = requests.post(
|
||||
f"https://api.daily.co/v1/meeting-tokens",
|
||||
headers={
|
||||
"Authorization": f"Bearer {key}"},
|
||||
headers={"Authorization": f"Bearer {key}"},
|
||||
json={
|
||||
"properties": {
|
||||
"room_name": room_name,
|
||||
"is_owner": True,
|
||||
"exp": expiration}},
|
||||
"properties": {"room_name": room_name, "is_owner": True, "exp": expiration}
|
||||
},
|
||||
)
|
||||
|
||||
if res.status_code != 200:
|
||||
raise Exception(
|
||||
f"Failed to create meeting token: {res.status_code} {res.text}")
|
||||
raise Exception(f"Failed to create meeting token: {res.status_code} {res.text}")
|
||||
|
||||
token: str = res.json()["token"]
|
||||
|
||||
@@ -45,17 +45,14 @@ async def main(room_url: str, token):
|
||||
print(f"finder: {finder}")
|
||||
if finder >= 0:
|
||||
async for audio in tts.run_tts(f"Resetting."):
|
||||
transport.output_queue.put(
|
||||
Frame(FrameType.AUDIO_FRAME, audio))
|
||||
transport.output_queue.put(Frame(FrameType.AUDIO_FRAME, audio))
|
||||
sentence = ""
|
||||
continue
|
||||
# todo: we could differentiate between transcriptions from
|
||||
# different participants
|
||||
# todo: we could differentiate between transcriptions from different participants
|
||||
sentence += f" {message['text']}"
|
||||
print(f"sentence is now: {sentence}")
|
||||
# TODO: Cache this audio
|
||||
phrase = random.choice(
|
||||
["OK.", "Got it.", "Sure.", "You bet.", "Sure thing."])
|
||||
phrase = random.choice(["OK.", "Got it.", "Sure.", "You bet.", "Sure thing."])
|
||||
async for audio in tts.run_tts(phrase):
|
||||
transport.output_queue.put(Frame(FrameType.AUDIO_FRAME, audio))
|
||||
img_result = img.run_image_gen(sentence, "1024x1024")
|
||||
@@ -85,11 +82,8 @@ async def main(room_url: str, token):
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Simple Daily Bot Sample")
|
||||
parser.add_argument(
|
||||
"-u",
|
||||
"--url",
|
||||
type=str,
|
||||
required=True,
|
||||
help="URL of the Daily room to join")
|
||||
"-u", "--url", type=str, required=True, help="URL of the Daily room to join"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-k",
|
||||
"--apikey",
|
||||
@@ -100,25 +94,20 @@ if __name__ == "__main__":
|
||||
|
||||
args, unknown = parser.parse_known_args()
|
||||
|
||||
# Create a meeting token for the given room with an expiration 1 hour in
|
||||
# the future.
|
||||
# Create a meeting token for the given room with an expiration 1 hour in the future.
|
||||
room_name: str = urllib.parse.urlparse(args.url).path[1:]
|
||||
expiration: float = time.time() + 60 * 60
|
||||
|
||||
res: requests.Response = requests.post(
|
||||
f"https://api.daily.co/v1/meeting-tokens",
|
||||
headers={
|
||||
"Authorization": f"Bearer {args.apikey}"},
|
||||
headers={"Authorization": f"Bearer {args.apikey}"},
|
||||
json={
|
||||
"properties": {
|
||||
"room_name": room_name,
|
||||
"is_owner": True,
|
||||
"exp": expiration}},
|
||||
"properties": {"room_name": room_name, "is_owner": True, "exp": expiration}
|
||||
},
|
||||
)
|
||||
|
||||
if res.status_code != 200:
|
||||
raise Exception(
|
||||
f"Failed to create meeting token: {res.status_code} {res.text}")
|
||||
raise Exception(f"Failed to create meeting token: {res.status_code} {res.text}")
|
||||
|
||||
token: str = res.json()["token"]
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from dailyai.services.ai_services import AIService, FrameLogger
|
||||
from dailyai.pipeline.frames import Frame, AudioFrame, LLMResponseEndFrame, LLMMessagesQueueFrame
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from examples.support.runner import configure
|
||||
from examples.foundational.support.runner import configure
|
||||
|
||||
sounds = {}
|
||||
sound_files = [
|
||||
|
||||
@@ -24,8 +24,7 @@ def get_meeting_token(room_name, daily_api_key, token_expiry):
|
||||
'is_owner': True,
|
||||
'exp': token_expiry}})
|
||||
if res.status_code != 200:
|
||||
return jsonify(
|
||||
{'error': 'Unable to create meeting token', 'detail': res.text}), 500
|
||||
return jsonify({'error': 'Unable to create meeting token', 'detail': res.text}), 500
|
||||
meeting_token = res.json()['token']
|
||||
return meeting_token
|
||||
|
||||
|
||||
@@ -14,16 +14,14 @@ load_dotenv()
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
print(
|
||||
f"I loaded an environment, and my FAL_KEY_ID is {os.getenv('FAL_KEY_ID')}")
|
||||
print(f"I loaded an environment, and my FAL_KEY_ID is {os.getenv('FAL_KEY_ID')}")
|
||||
|
||||
|
||||
def start_bot(bot_path, args=None):
|
||||
daily_api_key = os.getenv("DAILY_API_KEY")
|
||||
api_path = os.getenv("DAILY_API_PATH") or "https://api.daily.co/v1"
|
||||
|
||||
timeout = int(os.getenv("DAILY_ROOM_TIMEOUT")
|
||||
or os.getenv("DAILY_BOT_MAX_DURATION") or 300)
|
||||
timeout = int(os.getenv("DAILY_ROOM_TIMEOUT") or os.getenv("DAILY_BOT_MAX_DURATION") or 300)
|
||||
exp = time.time() + timeout
|
||||
res = requests.post(
|
||||
f"{api_path}/rooms",
|
||||
@@ -61,13 +59,14 @@ def start_bot(bot_path, args=None):
|
||||
extra_args = ""
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[f"python {bot_path} -u {room_url} -t {meeting_token} -k {daily_api_key} {extra_args}"],
|
||||
[
|
||||
f"python {bot_path} -u {room_url} -t {meeting_token} -k {daily_api_key} {extra_args}"
|
||||
],
|
||||
shell=True,
|
||||
bufsize=1,
|
||||
)
|
||||
|
||||
# Don't return until the bot has joined the room, but wait for at most 2
|
||||
# seconds.
|
||||
# Don't return until the bot has joined the room, but wait for at most 2 seconds.
|
||||
attempts = 0
|
||||
while attempts < 20:
|
||||
time.sleep(0.1)
|
||||
@@ -83,13 +82,11 @@ def start_bot(bot_path, args=None):
|
||||
# Additional client config
|
||||
config = {}
|
||||
if os.getenv("CLIENT_VAD_TIMEOUT_SEC"):
|
||||
config['vad_timeout_sec'] = float(
|
||||
os.getenv("DAILY_CLIENT_VAD_TIMEOUT_SEC"))
|
||||
config['vad_timeout_sec'] = float(os.getenv("DAILY_CLIENT_VAD_TIMEOUT_SEC"))
|
||||
else:
|
||||
config['vad_timeout_sec'] = 1.5
|
||||
|
||||
# return jsonify({"room_url": room_url, "token": meeting_token, "config":
|
||||
# config}), 200
|
||||
# return jsonify({"room_url": room_url, "token": meeting_token, "config": config}), 200
|
||||
return redirect(room_url, code=301)
|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 759 KiB |
|
Before Width: | Height: | Size: 884 KiB |
|
Before Width: | Height: | Size: 876 KiB |
|
Before Width: | Height: | Size: 881 KiB |
|
Before Width: | Height: | Size: 866 KiB |
|
Before Width: | Height: | Size: 874 KiB |
|
Before Width: | Height: | Size: 882 KiB |
|
Before Width: | Height: | Size: 885 KiB |
|
Before Width: | Height: | Size: 888 KiB |
|
Before Width: | Height: | Size: 890 KiB |
|
Before Width: | Height: | Size: 898 KiB |
|
Before Width: | Height: | Size: 836 KiB |
|
Before Width: | Height: | Size: 903 KiB |
|
Before Width: | Height: | Size: 908 KiB |
|
Before Width: | Height: | Size: 908 KiB |
|
Before Width: | Height: | Size: 905 KiB |
|
Before Width: | Height: | Size: 903 KiB |
|
Before Width: | Height: | Size: 866 KiB |
|
Before Width: | Height: | Size: 849 KiB |
|
Before Width: | Height: | Size: 866 KiB |
|
Before Width: | Height: | Size: 866 KiB |
|
Before Width: | Height: | Size: 864 KiB |
|
Before Width: | Height: | Size: 858 KiB |
|
Before Width: | Height: | Size: 875 KiB |
|
Before Width: | Height: | Size: 881 KiB |
@@ -1,144 +0,0 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import os
|
||||
from PIL import Image
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from dailyai.pipeline.aggregators import (
|
||||
LLMResponseAggregator,
|
||||
UserResponseAggregator,
|
||||
)
|
||||
from dailyai.pipeline.frames import (
|
||||
ImageFrame,
|
||||
SpriteFrame,
|
||||
Frame,
|
||||
LLMResponseEndFrame,
|
||||
LLMMessagesQueueFrame,
|
||||
AudioFrame,
|
||||
PipelineStartedFrame,
|
||||
)
|
||||
from dailyai.services.ai_services import AIService
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
sprites = []
|
||||
|
||||
script_dir = os.path.dirname(__file__)
|
||||
|
||||
for i in range(1, 26):
|
||||
# Build the full path to the image file
|
||||
full_path = os.path.join(script_dir, f"assets/robot0{i}.png")
|
||||
# Get the filename without the extension to use as the dictionary key
|
||||
# Open the image and convert it to bytes
|
||||
with Image.open(full_path) as img:
|
||||
sprites.append(img.tobytes())
|
||||
|
||||
flipped = sprites[::-1]
|
||||
sprites.extend(flipped)
|
||||
# When the bot isn't talking, show a static image of the cat listening
|
||||
quiet_frame = ImageFrame("", sprites[0])
|
||||
talking_frame = SpriteFrame(images=sprites)
|
||||
|
||||
|
||||
class TalkingAnimation(AIService):
|
||||
"""
|
||||
This class starts a talking animation when it receives an first AudioFrame,
|
||||
and then returns to a "quiet" sprite when it sees a LLMResponseEndFrame.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._is_talking = False
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, AudioFrame):
|
||||
if not self._is_talking:
|
||||
yield talking_frame
|
||||
yield frame
|
||||
self._is_talking = True
|
||||
else:
|
||||
yield frame
|
||||
elif isinstance(frame, LLMResponseEndFrame):
|
||||
yield quiet_frame
|
||||
yield frame
|
||||
self._is_talking = False
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class AnimationInitializer(AIService):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, PipelineStartedFrame):
|
||||
yield quiet_frame
|
||||
yield frame
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
async def main(room_url: str, token):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
token,
|
||||
"Chatbot",
|
||||
duration_minutes=5,
|
||||
start_transcription=True,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=True,
|
||||
camera_width=1024,
|
||||
camera_height=576,
|
||||
vad_enabled=True,
|
||||
)
|
||||
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id="pNInz6obpgDQGcFmaJgB",
|
||||
)
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
|
||||
ta = TalkingAnimation()
|
||||
ai = AnimationInitializer()
|
||||
pipeline = Pipeline([ai, llm, tts, ta])
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are Chatbot, a friendly, helpful robot. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio. Respond to what the user said in a creative and helpful way, but keep your responses brief. Start by introducing yourself.",
|
||||
},
|
||||
]
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await pipeline.queue_frames([LLMMessagesQueueFrame(messages)])
|
||||
|
||||
async def run_conversation():
|
||||
|
||||
await transport.run_interruptible_pipeline(
|
||||
pipeline,
|
||||
post_processor=LLMResponseAggregator(messages),
|
||||
pre_processor=UserResponseAggregator(messages),
|
||||
)
|
||||
|
||||
transport.transcription_settings["extra"]["endpointing"] = True
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
await asyncio.gather(transport.run(), run_conversation())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
(url, token) = configure()
|
||||
asyncio.run(main(url, token))
|
||||
@@ -1,353 +0,0 @@
|
||||
import copy
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import wave
|
||||
from typing import AsyncGenerator, List
|
||||
from PIL import Image
|
||||
from dailyai.pipeline.opeanai_llm_aggregator import (
|
||||
OpenAIAssistantContextAggregator,
|
||||
OpenAIUserContextAggregator,
|
||||
)
|
||||
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.openai_llm_context import OpenAILLMContext
|
||||
from dailyai.services.open_ai_services import OpenAILLMService
|
||||
from dailyai.services.deepgram_ai_services import DeepgramTTSService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from examples.support.runner import configure
|
||||
from dailyai.pipeline.frames import (
|
||||
OpenAILLMContextFrame,
|
||||
TranscriptionQueueFrame,
|
||||
Frame,
|
||||
LLMFunctionCallFrame,
|
||||
LLMFunctionStartFrame,
|
||||
AudioFrame,
|
||||
)
|
||||
from dailyai.services.ai_services import FrameLogger, AIService
|
||||
from openai._types import NotGiven, NOT_GIVEN
|
||||
|
||||
from openai.types.chat import (
|
||||
ChatCompletionToolParam,
|
||||
)
|
||||
|
||||
logging.basicConfig(format="%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
sounds = {}
|
||||
sound_files = [
|
||||
"clack-short.wav",
|
||||
"clack.wav",
|
||||
"clack-short-quiet.wav",
|
||||
"ding.wav",
|
||||
"ding2.wav",
|
||||
]
|
||||
|
||||
script_dir = os.path.dirname(__file__)
|
||||
|
||||
for file in sound_files:
|
||||
# Build the full path to the sound file
|
||||
full_path = os.path.join(script_dir, "assets", file)
|
||||
# Get the filename without the extension to use as the dictionary key
|
||||
filename = os.path.splitext(os.path.basename(full_path))[0]
|
||||
# Open the sound and convert it to bytes
|
||||
with wave.open(full_path) as audio_file:
|
||||
sounds[file] = audio_file.readframes(-1)
|
||||
|
||||
|
||||
steps = [{"prompt": "Start by introducing yourself. Then, ask the user to confirm their identity by telling you their birthday, including the year. When they answer with their birthday, call the verify_birthday function.",
|
||||
"run_async": False,
|
||||
"failed": "The user provided an incorrect birthday. Ask them for their birthday again. When they answer, call the verify_birthday function.",
|
||||
"tools": [{"type": "function",
|
||||
"function": {"name": "verify_birthday",
|
||||
"description": "Use this function to verify the user has provided their correct birthday.",
|
||||
"parameters": {"type": "object",
|
||||
"properties": {"birthday": {"type": "string",
|
||||
"description": "The user's birthdate, including the year. The user can provide it in any format, but convert it to YYYY-MM-DD format to call this function.",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
{"prompt": "Next, thank the user for confirming their identity, then ask the user to list their current prescriptions. Each prescription needs to have a medication name and a dosage. Do not call the list_prescriptions function with any unknown dosages.",
|
||||
"run_async": True,
|
||||
"tools": [{"type": "function",
|
||||
"function": {"name": "list_prescriptions",
|
||||
"description": "Once the user has provided a list of their prescription medications, call this function.",
|
||||
"parameters": {"type": "object",
|
||||
"properties": {"prescriptions": {"type": "array",
|
||||
"items": {"type": "object",
|
||||
"properties": {"medication": {"type": "string",
|
||||
"description": "The medication's name",
|
||||
},
|
||||
"dosage": {"type": "string",
|
||||
"description": "The prescription's dosage",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
{"prompt": "Next, ask the user if they have any allergies. Once they have listed their allergies or confirmed they don't have any, call the list_allergies function.",
|
||||
"run_async": True,
|
||||
"tools": [{"type": "function",
|
||||
"function": {"name": "list_allergies",
|
||||
"description": "Once the user has provided a list of their allergies, call this function.",
|
||||
"parameters": {"type": "object",
|
||||
"properties": {"allergies": {"type": "array",
|
||||
"items": {"type": "object",
|
||||
"properties": {"name": {"type": "string",
|
||||
"description": "What the user is allergic to",
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
{"prompt": "Now ask the user if they have any medical conditions the doctor should know about. Once they've answered the question, call the list_conditions function.",
|
||||
"run_async": True,
|
||||
"tools": [{"type": "function",
|
||||
"function": {"name": "list_conditions",
|
||||
"description": "Once the user has provided a list of their medical conditions, call this function.",
|
||||
"parameters": {"type": "object",
|
||||
"properties": {"conditions": {"type": "array",
|
||||
"items": {"type": "object",
|
||||
"properties": {"name": {"type": "string",
|
||||
"description": "The user's medical condition",
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{"prompt": "Finally, ask the user the reason for their doctor visit today. Once they answer, call the list_visit_reasons function.",
|
||||
"run_async": True,
|
||||
"tools": [{"type": "function",
|
||||
"function": {"name": "list_visit_reasons",
|
||||
"description": "Once the user has provided a list of the reasons they are visiting a doctor today, call this function.",
|
||||
"parameters": {"type": "object",
|
||||
"properties": {"visit_reasons": {"type": "array",
|
||||
"items": {"type": "object",
|
||||
"properties": {"name": {"type": "string",
|
||||
"description": "The user's reason for visiting the doctor",
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
{"prompt": "Now, thank the user and end the conversation.",
|
||||
"run_async": True,
|
||||
"tools": [],
|
||||
},
|
||||
{"prompt": "",
|
||||
"run_async": True,
|
||||
"tools": []},
|
||||
]
|
||||
current_step = 0
|
||||
|
||||
|
||||
class ChecklistProcessor(AIService):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: OpenAILLMContext,
|
||||
llm: AIService,
|
||||
tools: List[ChatCompletionToolParam] | NotGiven = NOT_GIVEN,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._context: OpenAILLMContext = context
|
||||
self._llm = llm
|
||||
self._id = "You are Jessica, an agent for a company called Tri-County Health Services. Your job is to collect important information from the user before their doctor visit. You're talking to Chad Bailey. You should address the user by their first name and be polite and professional. You're not a medical professional, so you shouldn't provide any advice. Keep your responses short. Your job is to collect information to give to a doctor. Don't make assumptions about what values to plug into functions. Ask for clarification if a user response is ambiguous."
|
||||
self._acks = ["One sec.", "Let me confirm that.", "Thanks.", "OK."]
|
||||
|
||||
# Create an allowlist of functions that the LLM can call
|
||||
self._functions = [
|
||||
"verify_birthday",
|
||||
"list_prescriptions",
|
||||
"list_allergies",
|
||||
"list_conditions",
|
||||
"list_visit_reasons",
|
||||
]
|
||||
|
||||
self._context.add_message(
|
||||
{"role": "system", "content": f"{self._id} {steps[0]['prompt']}"}
|
||||
)
|
||||
|
||||
if tools:
|
||||
self._context.set_tools(tools)
|
||||
|
||||
def verify_birthday(self, args):
|
||||
return args["birthday"] == "1983-01-01"
|
||||
|
||||
def list_prescriptions(self, args):
|
||||
# print(f"--- Prescriptions: {args['prescriptions']}\n")
|
||||
pass
|
||||
|
||||
def list_allergies(self, args):
|
||||
# print(f"--- Allergies: {args['allergies']}\n")
|
||||
pass
|
||||
|
||||
def list_conditions(self, args):
|
||||
# print(f"--- Medical Conditions: {args['conditions']}")
|
||||
pass
|
||||
|
||||
def list_visit_reasons(self, args):
|
||||
# print(f"Visit Reasons: {args['visit_reasons']}")
|
||||
pass
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
global current_step
|
||||
this_step = steps[current_step]
|
||||
self._context.set_tools(this_step["tools"])
|
||||
if isinstance(frame, LLMFunctionStartFrame):
|
||||
print(f"... Preparing function call: {frame.function_name}")
|
||||
self._function_name = frame.function_name
|
||||
if this_step["run_async"]:
|
||||
# Get the LLM talking about the next step before getting the rest
|
||||
# of the function call completion
|
||||
current_step += 1
|
||||
self._context.add_message(
|
||||
{"role": "system", "content": steps[current_step]["prompt"]}
|
||||
)
|
||||
yield OpenAILLMContextFrame(self._context)
|
||||
|
||||
local_context = copy.deepcopy(self._context)
|
||||
local_context.set_tool_choice("none")
|
||||
async for frame in llm.process_frame(
|
||||
OpenAILLMContextFrame(local_context)
|
||||
):
|
||||
yield frame
|
||||
else:
|
||||
# Insert a quick response while we run the function
|
||||
yield AudioFrame(sounds["ding2.wav"])
|
||||
pass
|
||||
elif isinstance(frame, LLMFunctionCallFrame):
|
||||
|
||||
if frame.function_name and frame.arguments:
|
||||
print(
|
||||
f"--> Calling function: {frame.function_name} with arguments:")
|
||||
pretty_json = re.sub(
|
||||
"\n", "\n ", json.dumps(
|
||||
json.loads(
|
||||
frame.arguments), indent=2))
|
||||
print(f"--> {pretty_json}\n")
|
||||
if frame.function_name not in self._functions:
|
||||
raise Exception(
|
||||
f"The LLM tried to call a function named {frame.function_name}, which isn't in the list of known functions. Please check your prompt and/or self._functions."
|
||||
)
|
||||
fn = getattr(self, frame.function_name)
|
||||
result = fn(json.loads(frame.arguments))
|
||||
|
||||
if not this_step["run_async"]:
|
||||
if result:
|
||||
current_step += 1
|
||||
self._context.add_message(
|
||||
{"role": "system", "content": steps[current_step]["prompt"]}
|
||||
)
|
||||
yield OpenAILLMContextFrame(self._context)
|
||||
|
||||
local_context = copy.deepcopy(self._context)
|
||||
local_context.set_tool_choice("none")
|
||||
async for frame in llm.process_frame(
|
||||
OpenAILLMContextFrame(local_context)
|
||||
):
|
||||
yield frame
|
||||
else:
|
||||
self._context.add_message(
|
||||
{"role": "system", "content": this_step["failed"]}
|
||||
)
|
||||
yield OpenAILLMContextFrame(self._context)
|
||||
|
||||
local_context = copy.deepcopy(self._context)
|
||||
local_context.set_tool_choice("none")
|
||||
async for frame in llm.process_frame(
|
||||
OpenAILLMContextFrame(local_context)
|
||||
):
|
||||
yield frame
|
||||
print(f"<-- Verify result: {result}\n")
|
||||
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
async def main(room_url: str, token):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
global transport
|
||||
global llm
|
||||
global tts
|
||||
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
token,
|
||||
"Intake Bot",
|
||||
5,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=False,
|
||||
start_transcription=True,
|
||||
vad_enabled=True,
|
||||
)
|
||||
|
||||
messages = []
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-1106-preview",
|
||||
)
|
||||
# tts = DeepgramTTSService(
|
||||
# aiohttp_session=session,
|
||||
# api_key=os.getenv("DEEPGRAM_API_KEY"),
|
||||
# voice="aura-asteria-en",
|
||||
# )
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id="XrExE9yKIg1WjnnlVkGX",
|
||||
)
|
||||
context = OpenAILLMContext(
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
checklist = ChecklistProcessor(context, llm)
|
||||
fl = FrameLogger("FRAME LOGGER 1:")
|
||||
fl2 = FrameLogger("FRAME LOGGER 2:")
|
||||
pipeline = Pipeline(processors=[fl, llm, fl2, checklist, tts])
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await pipeline.queue_frames([OpenAILLMContextFrame(context)])
|
||||
|
||||
async def handle_intake():
|
||||
await transport.run_interruptible_pipeline(
|
||||
pipeline,
|
||||
post_processor=OpenAIAssistantContextAggregator(context),
|
||||
pre_processor=OpenAIUserContextAggregator(context),
|
||||
)
|
||||
|
||||
transport.transcription_settings["extra"]["endpointing"] = True
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
try:
|
||||
await asyncio.gather(transport.run(), handle_intake())
|
||||
except (asyncio.CancelledError, KeyboardInterrupt):
|
||||
print("whoops")
|
||||
transport.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
(url, token) = configure()
|
||||
asyncio.run(main(url, token))
|
||||
@@ -1,294 +0,0 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import wave
|
||||
from typing import AsyncGenerator
|
||||
from PIL import Image
|
||||
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.pipeline.frame_processor import FrameProcessor
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.azure_ai_services import AzureLLMService, AzureTTSService
|
||||
from dailyai.services.fal_ai_services import FalImageGenService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService
|
||||
from dailyai.services.deepgram_ai_services import DeepgramTTSService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.pipeline.aggregators import (
|
||||
LLMAssistantContextAggregator,
|
||||
LLMContextAggregator,
|
||||
LLMUserContextAggregator,
|
||||
ParallelPipeline,
|
||||
UserResponseAggregator,
|
||||
LLMResponseAggregator,
|
||||
)
|
||||
from examples.support.runner import configure
|
||||
from dailyai.pipeline.frames import (
|
||||
EndPipeFrame,
|
||||
LLMMessagesQueueFrame,
|
||||
TranscriptionQueueFrame,
|
||||
Frame,
|
||||
TextFrame,
|
||||
LLMFunctionCallFrame,
|
||||
LLMFunctionStartFrame,
|
||||
LLMResponseEndFrame,
|
||||
StartFrame,
|
||||
AudioFrame,
|
||||
SpriteFrame,
|
||||
ImageFrame,
|
||||
UserStoppedSpeakingFrame,
|
||||
)
|
||||
from dailyai.services.ai_services import FrameLogger, AIService
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
sounds = {}
|
||||
images = {}
|
||||
sound_files = ["talking.wav", "listening.wav", "ding3.wav"]
|
||||
image_files = ["grandma-writing.png", "grandma-listening.png"]
|
||||
script_dir = os.path.dirname(__file__)
|
||||
|
||||
for file in sound_files:
|
||||
# Build the full path to the sound file
|
||||
full_path = os.path.join(script_dir, "assets", file)
|
||||
# Get the filename without the extension to use as the dictionary key
|
||||
filename = os.path.splitext(os.path.basename(full_path))[0]
|
||||
# Open the sound and convert it to bytes
|
||||
with wave.open(full_path) as audio_file:
|
||||
sounds[file] = audio_file.readframes(-1)
|
||||
|
||||
for file in image_files:
|
||||
# Build the full path to the image file
|
||||
full_path = os.path.join(script_dir, "assets", file)
|
||||
# Get the filename without the extension to use as the dictionary key
|
||||
filename = os.path.splitext(os.path.basename(full_path))[0]
|
||||
# Open the image and convert it to bytes
|
||||
with Image.open(full_path) as img:
|
||||
images[file] = img.tobytes()
|
||||
|
||||
|
||||
class StoryStartFrame(TextFrame):
|
||||
pass
|
||||
|
||||
|
||||
class StoryPageFrame(TextFrame):
|
||||
pass
|
||||
|
||||
|
||||
class StoryPromptFrame(TextFrame):
|
||||
pass
|
||||
|
||||
|
||||
class StoryProcessor(FrameProcessor):
|
||||
def __init__(self, messages, story):
|
||||
self._messages = messages
|
||||
self._text = ""
|
||||
self._story = story
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
"""
|
||||
The response from the LLM service looks like:
|
||||
A comment about the user's choice
|
||||
[start] (when the cat starts telling parts of the story)
|
||||
A sentence of the story
|
||||
[break] (between each sentence/'page' of the story)
|
||||
[prompt] (when the cat asks the user to make a decision)
|
||||
Question about the next part of the story
|
||||
|
||||
1. Catch the frames that are generated by the LLM service
|
||||
"""
|
||||
if isinstance(frame, UserStoppedSpeakingFrame):
|
||||
yield ImageFrame(None, images["grandma-writing.png"])
|
||||
yield AudioFrame(sounds["talking.wav"])
|
||||
|
||||
elif isinstance(frame, TextFrame):
|
||||
self._text += frame.text
|
||||
|
||||
if re.findall(r".*\[[sS]tart\].*", self._text):
|
||||
# Then we have the intro. Send it to speech ASAP
|
||||
self._text = self._text.replace("[Start]", "")
|
||||
self._text = self._text.replace("[start]", "")
|
||||
|
||||
self._text = self._text.replace("\n", " ")
|
||||
if len(self._text) > 2:
|
||||
yield ImageFrame(None, images["grandma-writing.png"])
|
||||
yield StoryStartFrame(self._text)
|
||||
yield AudioFrame(sounds["ding3.wav"])
|
||||
self._text = ""
|
||||
|
||||
elif re.findall(r".*\[[bB]reak\].*", self._text):
|
||||
# Then it's a page of the story. Get an image too
|
||||
self._text = self._text.replace("[Break]", "")
|
||||
self._text = self._text.replace("[break]", "")
|
||||
self._text = self._text.replace("\n", " ")
|
||||
if len(self._text) > 2:
|
||||
self._story.append(self._text)
|
||||
yield StoryPageFrame(self._text)
|
||||
yield AudioFrame(sounds["ding3.wav"])
|
||||
|
||||
self._text = ""
|
||||
elif re.findall(r".*\[[pP]rompt\].*", self._text):
|
||||
# Then it's question time. Flush any
|
||||
# text here as a story page, then set
|
||||
# the var to get to prompt mode
|
||||
# cb: trying scene now
|
||||
# self.handle_chunk(self._text)
|
||||
self._text = self._text.replace("[Prompt]", "")
|
||||
self._text = self._text.replace("[prompt]", "")
|
||||
|
||||
self._text = self._text.replace("\n", " ")
|
||||
if len(self._text) > 2:
|
||||
self._story.append(self._text)
|
||||
yield StoryPageFrame(self._text)
|
||||
else:
|
||||
# After the prompt thing, we'll catch an LLM end to get the
|
||||
# last bit
|
||||
pass
|
||||
elif isinstance(frame, LLMResponseEndFrame):
|
||||
yield ImageFrame(None, images["grandma-writing.png"])
|
||||
yield StoryPromptFrame(self._text)
|
||||
self._text = ""
|
||||
yield frame
|
||||
yield ImageFrame(None, images["grandma-listening.png"])
|
||||
yield AudioFrame(sounds["listening.wav"])
|
||||
|
||||
else:
|
||||
# pass through everything that's not a TextFrame
|
||||
yield frame
|
||||
|
||||
|
||||
class StoryImageGenerator(FrameProcessor):
|
||||
def __init__(self, story, llm, img):
|
||||
self._story = story
|
||||
self._llm = llm
|
||||
self._img = img
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, StoryPageFrame):
|
||||
if len(self._story) == 1:
|
||||
prompt = f'You are an illustrator for a children\'s story book. Generate a prompt for DALL-E to create an illustration for the first page of the book, which reads: "{self._story[0]}"\n\n Your response should start with the phrase "Children\'s book illustration of".'
|
||||
else:
|
||||
prompt = f"You are an illustrator for a children's story book. Here is the story so far:\n\n\"{' '.join(self._story[:-1])}\"\n\nGenerate a prompt for DALL-E to create an illustration for the next page. Here's the sentence for the next page:\n\n\"{self._story[-1:][0]}\"\n\n Your response should start with the phrase \"Children's book illustration of\"."
|
||||
msgs = [{"role": "system", "content": prompt}]
|
||||
image_prompt = ""
|
||||
async for f in self._llm.process_frame(LLMMessagesQueueFrame(msgs)):
|
||||
if isinstance(f, TextFrame):
|
||||
image_prompt += f.text
|
||||
async for f in self._img.process_frame(TextFrame(image_prompt)):
|
||||
yield f
|
||||
# Yield the original StoryPageFrame for basic image/audio sync
|
||||
yield frame
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
async def main(room_url: str, token):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a storytelling grandma who loves to make up fantastic, fun, and educational stories for children between the ages of 5 and 10 years old. Your stories are full of friendly, magical creatures. Your stories are never scary. Each sentence of your story will become a page in a storybook. Stop after 3-4 sentences and give the child a choice to make that will influence the next part of the story. Once the child responds, start by saying something nice about the choice they made, then include [start] in your response. Include [break] after each sentence of the story. Include [prompt] between the story and the prompt.",
|
||||
}
|
||||
]
|
||||
|
||||
story = []
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-1106-preview",
|
||||
) # gpt-4-1106-preview
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id="Xb7hH8MSUJpSbSDYk0k2",
|
||||
) # matilda
|
||||
img = FalImageGenService(
|
||||
image_size="1024x1024",
|
||||
aiohttp_session=session,
|
||||
key_id=os.getenv("FAL_KEY_ID"),
|
||||
key_secret=os.getenv("FAL_KEY_SECRET"),
|
||||
)
|
||||
lra = LLMResponseAggregator(messages)
|
||||
ura = UserResponseAggregator(messages)
|
||||
sp = StoryProcessor(messages, story)
|
||||
sig = StoryImageGenerator(story, llm, img)
|
||||
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
token,
|
||||
"Storybot",
|
||||
5,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=True,
|
||||
camera_width=1024,
|
||||
camera_height=1024,
|
||||
start_transcription=True,
|
||||
vad_enabled=True,
|
||||
vad_stop_s=1.5,
|
||||
)
|
||||
|
||||
start_story_event = asyncio.Event()
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
start_story_event.set()
|
||||
|
||||
async def storytime():
|
||||
await start_story_event.wait()
|
||||
|
||||
# We're being a bit tricky here by using a special system prompt to
|
||||
# ask the user for a story topic. After their intial response, we'll
|
||||
# use a different system prompt to create story pages.
|
||||
intro_messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a storytelling grandma who loves to make up fantastic, fun, and educational stories for children between the ages of 5 and 10 years old. Your stories are full of friendly, magical creatures. Your stories are never scary. Begin by asking what a child wants you to tell a story about. Keep your reponse to only a few sentences.",
|
||||
}
|
||||
]
|
||||
lca = LLMAssistantContextAggregator(messages)
|
||||
local_pipeline = Pipeline(
|
||||
[llm, lca, tts], sink=transport.send_queue)
|
||||
await local_pipeline.queue_frames(
|
||||
[
|
||||
ImageFrame(None, images["grandma-listening.png"]),
|
||||
LLMMessagesQueueFrame(intro_messages),
|
||||
AudioFrame(sounds["listening.wav"]),
|
||||
EndPipeFrame(),
|
||||
]
|
||||
)
|
||||
await local_pipeline.run_pipeline()
|
||||
|
||||
fl = FrameLogger("### After Image Generation")
|
||||
pipeline = Pipeline(
|
||||
processors=[
|
||||
ura,
|
||||
llm,
|
||||
sp,
|
||||
sig,
|
||||
fl,
|
||||
tts,
|
||||
lra,
|
||||
]
|
||||
)
|
||||
await transport.run_pipeline(
|
||||
pipeline,
|
||||
)
|
||||
|
||||
transport.transcription_settings["extra"]["endpointing"] = True
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
try:
|
||||
await asyncio.gather(transport.run(), storytime())
|
||||
except (asyncio.CancelledError, KeyboardInterrupt):
|
||||
print("whoops")
|
||||
transport.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
(url, token) = configure()
|
||||
asyncio.run(main(url, token))
|
||||
@@ -1,100 +0,0 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import os
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from dailyai.pipeline.frames import Frame, LLMMessagesQueueFrame, RequestVideoImageFrame, LLMResponseEndFrame
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.pipeline.frame_processor import FrameProcessor
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService, OpenAIVisionService
|
||||
from dailyai.services.fal_ai_services import FalImageGenService
|
||||
from dailyai.services.deepgram_ai_services import DeepgramTTSService
|
||||
from dailyai.services.ai_services import FrameLogger
|
||||
from dailyai.pipeline.aggregators import (
|
||||
LLMAssistantContextAggregator,
|
||||
LLMUserContextAggregator,
|
||||
)
|
||||
from dailyai.pipeline.frames import VideoImageFrame, VisionFrame
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class VideoImageFrameProcessor(FrameProcessor):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, VideoImageFrame):
|
||||
yield VisionFrame("Describe the image in one sentence.", frame.image)
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class ImageRefresher(FrameProcessor):
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, LLMResponseEndFrame):
|
||||
yield RequestVideoImageFrame(participantId=None)
|
||||
yield frame
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
async def main(room_url: str, token):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
token,
|
||||
"Respond bot",
|
||||
duration_minutes=5,
|
||||
start_transcription=True,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=True,
|
||||
camera_width=1024,
|
||||
camera_height=1024,
|
||||
vad_enabled=False,
|
||||
receive_video=True,
|
||||
receive_video_fps=0
|
||||
)
|
||||
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"),
|
||||
)
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
|
||||
vs = OpenAIVisionService(api_key=os.getenv("OPENAI_CHATGPT_API_KEY"))
|
||||
vifp = VideoImageFrameProcessor()
|
||||
ir = ImageRefresher()
|
||||
|
||||
pipeline = Pipeline(
|
||||
processors=[
|
||||
vifp,
|
||||
vs,
|
||||
tts,
|
||||
ir,
|
||||
],
|
||||
)
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await pipeline.queue_frames([RequestVideoImageFrame(participantId=None)])
|
||||
|
||||
transport.transcription_settings["extra"]["endpointing"] = True
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
await transport.run(pipeline)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
(url, token) = configure()
|
||||
asyncio.run(main(url, token))
|
||||
@@ -1,112 +0,0 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import os
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from dailyai.pipeline.frames import Frame, LLMMessagesQueueFrame, RequestVideoImageFrame, LLMResponseEndFrame, UserStartedSpeakingFrame, UserStoppedSpeakingFrame, TranscriptionQueueFrame, TextFrame
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.pipeline.frame_processor import FrameProcessor
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService, OpenAIVisionService
|
||||
from dailyai.services.fal_ai_services import FalImageGenService
|
||||
from dailyai.services.deepgram_ai_services import DeepgramTTSService
|
||||
from dailyai.services.ai_services import FrameLogger
|
||||
from dailyai.pipeline.aggregators import (
|
||||
LLMAssistantContextAggregator,
|
||||
LLMUserContextAggregator,
|
||||
)
|
||||
from dailyai.pipeline.frames import VideoImageFrame, VisionFrame
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class VADAggregator(FrameProcessor):
|
||||
def __init__(self):
|
||||
self.aggregating = False
|
||||
self.aggregation = ""
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, UserStartedSpeakingFrame):
|
||||
self.aggregating = True
|
||||
elif isinstance(frame, UserStoppedSpeakingFrame):
|
||||
self.aggregating = False
|
||||
# Sometimes VAD triggers quickly on and off. If we don't get any transcription,
|
||||
# it creates empty LLM message queue frames
|
||||
if len(self.aggregation) > 0:
|
||||
yield TextFrame(self.aggregation)
|
||||
|
||||
self.aggregation = ""
|
||||
yield frame
|
||||
elif isinstance(frame, TranscriptionQueueFrame) and self.aggregating:
|
||||
self.aggregation += f" {frame.text}"
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
async def main(room_url: str, token):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
token,
|
||||
"Respond bot",
|
||||
duration_minutes=5,
|
||||
start_transcription=True,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=True,
|
||||
camera_width=1024,
|
||||
camera_height=1024,
|
||||
vad_enabled=True,
|
||||
receive_video=True,
|
||||
receive_video_fps=0,
|
||||
vad_timeout_s=1.0
|
||||
)
|
||||
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
voice_id=os.getenv("ELEVENLABS_VOICE_ID"),
|
||||
)
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
|
||||
vs = OpenAIVisionService(api_key=os.getenv("OPENAI_CHATGPT_API_KEY"))
|
||||
vad = VADAggregator()
|
||||
img = FalImageGenService(
|
||||
image_size="1024x1024",
|
||||
aiohttp_session=session,
|
||||
key_id=os.getenv("FAL_KEY_ID"),
|
||||
key_secret=os.getenv("FAL_KEY_SECRET"),
|
||||
)
|
||||
fl = FrameLogger("!!! Start")
|
||||
fl2 = FrameLogger("!!! AFTER VAD")
|
||||
fl3 = FrameLogger("!!! After img")
|
||||
pipeline = Pipeline(
|
||||
processors=[
|
||||
fl,
|
||||
vad,
|
||||
fl2,
|
||||
img,
|
||||
fl3
|
||||
],
|
||||
)
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await pipeline.queue_frames([RequestVideoImageFrame(participantId=None)])
|
||||
|
||||
transport.transcription_settings["extra"]["endpointing"] = True
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
await transport.run(pipeline)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
(url, token) = configure()
|
||||
asyncio.run(main(url, token))
|
||||
@@ -1,210 +0,0 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from dailyai.pipeline.frames import Frame, LLMMessagesQueueFrame, RequestVideoImageFrame, LLMResponseEndFrame, TelestratorImageFrame, ImageFrame, TextFrame
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.pipeline.frame_processor import FrameProcessor
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService, OpenAIVisionService
|
||||
from dailyai.services.fal_ai_services import FalImageGenService
|
||||
from dailyai.services.deepgram_ai_services import DeepgramTTSService
|
||||
from dailyai.services.ai_services import FrameLogger
|
||||
from dailyai.pipeline.aggregators import (
|
||||
LLMAssistantContextAggregator,
|
||||
LLMUserContextAggregator,
|
||||
LLMFullResponseAggregator
|
||||
)
|
||||
from dailyai.pipeline.frames import VideoImageFrame, VisionFrame
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
narrators = [{"voice_id": "wDRBdcyPzQOCeq51IxW5",
|
||||
"prompt": "Describe the image in one sentence."},
|
||||
{"voice_id": "M3bAX0o3Ptb2l6XqwQJV",
|
||||
"prompt": "Describe the image in one sentence, in the style of John Oliver's Last Week Tonight show."},
|
||||
{"voice_id": "lJm5d2ZZ3UE4qYOxl2t7",
|
||||
"prompt": "Describe the image in one sentence, in the style of Oprah Winfrey."},
|
||||
{"voice_id": "7SNUlQ8GAbnZxRO9CKOt",
|
||||
"prompt": "Describe the image in one sentence, in the style of a royal pronouncement by the Queen of England."},
|
||||
{"voice_id": "gvpBhHjzfd7M2WedYVUI",
|
||||
"prompt": "Describe the image in one sentence, in the style of Captain Picard from Star Trek."},
|
||||
{"voice_id": "bnyr1EF3snReVXauGBNn",
|
||||
"prompt": "Describe the image in one sentence, in the style of Maya Angelou."}]
|
||||
|
||||
# random.shuffle(narrators)
|
||||
print(f"$$$ narrators: {narrators}")
|
||||
narrator = {"narrator": narrators[0]}
|
||||
|
||||
|
||||
class TranslationProcessor(FrameProcessor):
|
||||
def __init__(self, in_language, out_language):
|
||||
self._in_language = in_language
|
||||
self._out_language = out_language
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, TextFrame):
|
||||
context = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"You will be provided with a sentence in {self._in_language}, and your task is to translate it into {self._out_language}.",
|
||||
},
|
||||
{"role": "user", "content": frame.text},
|
||||
]
|
||||
|
||||
yield LLMMessagesQueueFrame(context)
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class NarratorShuffle(FrameProcessor):
|
||||
def __init__(self, narrator, narrators):
|
||||
self._narrator = narrator
|
||||
self._narrators = narrators
|
||||
self._i = 0
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, (ImageFrame, TelestratorImageFrame)):
|
||||
self._i += 1
|
||||
if self._i >= len(self._narrators):
|
||||
print(f"### shuffling narrators")
|
||||
random.shuffle(self._narrators)
|
||||
self._i = 0
|
||||
|
||||
self._narrator["narrator"] = self._narrators[self._i]
|
||||
print(f"### new narrator is {self._narrator}")
|
||||
yield frame
|
||||
|
||||
|
||||
class VideoImageFrameProcessor(FrameProcessor):
|
||||
def __init__(self, narrator):
|
||||
self._narrator = narrator
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, (VideoImageFrame, TelestratorImageFrame)):
|
||||
yield VisionFrame(self._narrator["narrator"]["prompt"], frame.image)
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class ImageRefresher(FrameProcessor):
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, LLMResponseEndFrame):
|
||||
yield RequestVideoImageFrame(participantId=None)
|
||||
yield frame
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class TelestratorImageWrapper(FrameProcessor):
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, ImageFrame):
|
||||
yield TelestratorImageFrame(None, frame.image)
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
async def main(room_url: str, token):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
token,
|
||||
"Respond bot",
|
||||
duration_minutes=5,
|
||||
start_transcription=True,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=True,
|
||||
camera_width=1024,
|
||||
camera_height=576,
|
||||
vad_enabled=False,
|
||||
receive_video=True,
|
||||
receive_video_fps=0
|
||||
)
|
||||
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
narrator=narrator,
|
||||
aggregate_sentences=False
|
||||
)
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
|
||||
vs = OpenAIVisionService(api_key=os.getenv("OPENAI_CHATGPT_API_KEY"))
|
||||
vifp = VideoImageFrameProcessor(narrator)
|
||||
ir = ImageRefresher()
|
||||
img = FalImageGenService(
|
||||
image_size="1024x1024",
|
||||
aiohttp_session=session,
|
||||
key_id=os.getenv("FAL_KEY_ID"),
|
||||
key_secret=os.getenv("FAL_KEY_SECRET"),
|
||||
)
|
||||
tiw = TelestratorImageWrapper()
|
||||
lfra = LLMFullResponseAggregator()
|
||||
lfra1 = LLMFullResponseAggregator()
|
||||
lfra2 = LLMFullResponseAggregator()
|
||||
lfra3 = LLMFullResponseAggregator()
|
||||
lfra4 = LLMFullResponseAggregator()
|
||||
fl0 = FrameLogger("@@@ About to describe")
|
||||
fl1 = FrameLogger("!!! About to image gen")
|
||||
f4 = FrameLogger("((( partway through )))")
|
||||
f5 = FrameLogger("!!! f5")
|
||||
ns = NarratorShuffle(narrator, narrators)
|
||||
t1 = TranslationProcessor("English", "Spanish")
|
||||
t2 = TranslationProcessor("Spanish", "German")
|
||||
t3 = TranslationProcessor("German", "Japanese")
|
||||
t4 = TranslationProcessor("Japanese", "English")
|
||||
pipeline = Pipeline(
|
||||
processors=[
|
||||
fl0,
|
||||
vifp,
|
||||
vs,
|
||||
lfra,
|
||||
tts,
|
||||
f4,
|
||||
t1,
|
||||
llm,
|
||||
lfra1,
|
||||
f5,
|
||||
tts,
|
||||
|
||||
t2,
|
||||
llm,
|
||||
lfra2,
|
||||
tts,
|
||||
t3,
|
||||
llm,
|
||||
lfra3,
|
||||
tts,
|
||||
t4,
|
||||
llm,
|
||||
lfra4,
|
||||
tts,
|
||||
fl1,
|
||||
img,
|
||||
tiw,
|
||||
],
|
||||
)
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await pipeline.queue_frames([RequestVideoImageFrame(participantId=None)])
|
||||
|
||||
transport.transcription_settings["extra"]["endpointing"] = True
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
await transport.run(pipeline)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
(url, token) = configure()
|
||||
asyncio.run(main(url, token))
|
||||
@@ -1,191 +0,0 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from dailyai.pipeline.frames import Frame, LLMMessagesQueueFrame, RequestVideoImageFrame, LLMResponseEndFrame, TelestratorImageFrame, ImageFrame, TextFrame
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.pipeline.frame_processor import FrameProcessor
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService, OpenAIVisionService
|
||||
from dailyai.services.fal_ai_services import FalImageGenService
|
||||
from dailyai.services.deepgram_ai_services import DeepgramTTSService
|
||||
from dailyai.services.ai_services import FrameLogger
|
||||
from dailyai.pipeline.aggregators import (
|
||||
LLMAssistantContextAggregator,
|
||||
LLMUserContextAggregator,
|
||||
LLMFullResponseAggregator
|
||||
)
|
||||
from dailyai.pipeline.frames import VideoImageFrame, VisionFrame
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
narrators = [{"voice_id": "wDRBdcyPzQOCeq51IxW5",
|
||||
"prompt": "Describe the image in a haiku."},
|
||||
{"voice_id": "M3bAX0o3Ptb2l6XqwQJV",
|
||||
"prompt": "Describe the image in one sentence, in the style of John Oliver's Last Week Tonight show."},
|
||||
{"voice_id": "lJm5d2ZZ3UE4qYOxl2t7",
|
||||
"prompt": "Describe the image in one sentence, in the style of Oprah Winfrey."},
|
||||
{"voice_id": "7SNUlQ8GAbnZxRO9CKOt",
|
||||
"prompt": "Describe the image in one sentence, in the style of a royal pronouncement by the Queen of England."},
|
||||
{"voice_id": "gvpBhHjzfd7M2WedYVUI",
|
||||
"prompt": "Describe the image in one sentence, in the style of Captain Picard from Star Trek."},
|
||||
{"voice_id": "bnyr1EF3snReVXauGBNn",
|
||||
"prompt": "Describe the image in one sentence, in the style of Maya Angelou."}]
|
||||
|
||||
# random.shuffle(narrators)
|
||||
print(f"$$$ narrators: {narrators}")
|
||||
narrator = {"narrator": narrators[0]}
|
||||
|
||||
|
||||
class TranslationProcessor(FrameProcessor):
|
||||
def __init__(self, in_language, out_language):
|
||||
self._in_language = in_language
|
||||
self._out_language = out_language
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, TextFrame):
|
||||
context = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"You will be provided with a sentence in {self._in_language}, and your task is to translate it into {self._out_language}.",
|
||||
},
|
||||
{"role": "user", "content": frame.text},
|
||||
]
|
||||
|
||||
yield LLMMessagesQueueFrame(context)
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class NarratorShuffle(FrameProcessor):
|
||||
def __init__(self, narrator, narrators):
|
||||
self._narrator = narrator
|
||||
self._narrators = narrators
|
||||
self._i = 0
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, (ImageFrame, TelestratorImageFrame)):
|
||||
self._i += 1
|
||||
if self._i >= len(self._narrators):
|
||||
print(f"### shuffling narrators")
|
||||
random.shuffle(self._narrators)
|
||||
self._i = 0
|
||||
|
||||
self._narrator["narrator"] = self._narrators[self._i]
|
||||
print(f"### new narrator is {self._narrator}")
|
||||
yield frame
|
||||
|
||||
|
||||
class VideoImageFrameProcessor(FrameProcessor):
|
||||
def __init__(self, narrator):
|
||||
self._narrator = narrator
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, (VideoImageFrame, TelestratorImageFrame)):
|
||||
yield VisionFrame(self._narrator["narrator"]["prompt"], frame.image)
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class ImageRefresher(FrameProcessor):
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, LLMResponseEndFrame):
|
||||
yield RequestVideoImageFrame(participantId=None)
|
||||
yield frame
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class TelestratorImageWrapper(FrameProcessor):
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, ImageFrame):
|
||||
yield TelestratorImageFrame(None, frame.image)
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
async def main(room_url: str, token):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
token,
|
||||
"Respond bot",
|
||||
duration_minutes=5,
|
||||
start_transcription=True,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=True,
|
||||
camera_width=1024,
|
||||
camera_height=1024,
|
||||
vad_enabled=False,
|
||||
receive_video=True,
|
||||
receive_video_fps=0
|
||||
)
|
||||
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
narrator=narrator,
|
||||
aggregate_sentences=False
|
||||
)
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
|
||||
vs = OpenAIVisionService(api_key=os.getenv("OPENAI_CHATGPT_API_KEY"))
|
||||
vifp = VideoImageFrameProcessor(narrator)
|
||||
ir = ImageRefresher()
|
||||
img = FalImageGenService(
|
||||
image_size="1024x1024",
|
||||
aiohttp_session=session,
|
||||
key_id=os.getenv("FAL_KEY_ID"),
|
||||
key_secret=os.getenv("FAL_KEY_SECRET"),
|
||||
)
|
||||
tiw = TelestratorImageWrapper()
|
||||
lfra = LLMFullResponseAggregator()
|
||||
lfra1 = LLMFullResponseAggregator()
|
||||
lfra2 = LLMFullResponseAggregator()
|
||||
lfra3 = LLMFullResponseAggregator()
|
||||
lfra4 = LLMFullResponseAggregator()
|
||||
fl0 = FrameLogger("@@@ About to describe")
|
||||
fl1 = FrameLogger("!!! About to image gen")
|
||||
f4 = FrameLogger("((( partway through )))")
|
||||
f5 = FrameLogger("!!! f5")
|
||||
ns = NarratorShuffle(narrator, narrators)
|
||||
t1 = TranslationProcessor("English", "Spanish")
|
||||
t2 = TranslationProcessor("Spanish", "German")
|
||||
t3 = TranslationProcessor("German", "Japanese")
|
||||
t4 = TranslationProcessor("Japanese", "English")
|
||||
pipeline = Pipeline(
|
||||
processors=[
|
||||
fl0,
|
||||
vifp,
|
||||
vs,
|
||||
lfra,
|
||||
tts,
|
||||
fl1,
|
||||
img,
|
||||
tiw,
|
||||
],
|
||||
)
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await pipeline.queue_frames([RequestVideoImageFrame(participantId=None)])
|
||||
|
||||
transport.transcription_settings["extra"]["endpointing"] = True
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
await transport.run(pipeline)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
(url, token) = configure()
|
||||
asyncio.run(main(url, token))
|
||||
@@ -1,191 +0,0 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from dailyai.pipeline.frames import Frame, LLMMessagesQueueFrame, RequestVideoImageFrame, LLMResponseEndFrame, TelestratorImageFrame, ImageFrame, TextFrame
|
||||
from dailyai.pipeline.pipeline import Pipeline
|
||||
from dailyai.pipeline.frame_processor import FrameProcessor
|
||||
from dailyai.services.daily_transport_service import DailyTransportService
|
||||
from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService
|
||||
from dailyai.services.open_ai_services import OpenAILLMService, OpenAIVisionService
|
||||
from dailyai.services.fal_ai_services import FalImageGenService
|
||||
from dailyai.services.deepgram_ai_services import DeepgramTTSService
|
||||
from dailyai.services.ai_services import FrameLogger
|
||||
from dailyai.pipeline.aggregators import (
|
||||
LLMAssistantContextAggregator,
|
||||
LLMUserContextAggregator,
|
||||
LLMFullResponseAggregator
|
||||
)
|
||||
from dailyai.pipeline.frames import VideoImageFrame, VisionFrame
|
||||
from examples.support.runner import configure
|
||||
|
||||
logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s")
|
||||
logger = logging.getLogger("dailyai")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
narrators = [{"voice_id": "wDRBdcyPzQOCeq51IxW5",
|
||||
"prompt": "Describe the image in nine words."},
|
||||
{"voice_id": "M3bAX0o3Ptb2l6XqwQJV",
|
||||
"prompt": "Describe the image in one sentence, in the style of John Oliver's Last Week Tonight show."},
|
||||
{"voice_id": "lJm5d2ZZ3UE4qYOxl2t7",
|
||||
"prompt": "Describe the image in one sentence, in the style of Oprah Winfrey."},
|
||||
{"voice_id": "7SNUlQ8GAbnZxRO9CKOt",
|
||||
"prompt": "Describe the image in one sentence, in the style of a royal pronouncement by the Queen of England."},
|
||||
{"voice_id": "gvpBhHjzfd7M2WedYVUI",
|
||||
"prompt": "Describe the image in one sentence, in the style of Captain Picard from Star Trek."},
|
||||
{"voice_id": "bnyr1EF3snReVXauGBNn",
|
||||
"prompt": "Describe the image in one sentence, in the style of Maya Angelou."}]
|
||||
|
||||
# random.shuffle(narrators)
|
||||
print(f"$$$ narrators: {narrators}")
|
||||
narrator = {"narrator": narrators[0]}
|
||||
|
||||
|
||||
class TranslationProcessor(FrameProcessor):
|
||||
def __init__(self, in_language, out_language):
|
||||
self._in_language = in_language
|
||||
self._out_language = out_language
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, TextFrame):
|
||||
context = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"You will be provided with a sentence in {self._in_language}, and your task is to translate it into {self._out_language}.",
|
||||
},
|
||||
{"role": "user", "content": frame.text},
|
||||
]
|
||||
|
||||
yield LLMMessagesQueueFrame(context)
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class NarratorShuffle(FrameProcessor):
|
||||
def __init__(self, narrator, narrators):
|
||||
self._narrator = narrator
|
||||
self._narrators = narrators
|
||||
self._i = 0
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, (ImageFrame, TelestratorImageFrame)):
|
||||
self._i += 1
|
||||
if self._i >= len(self._narrators):
|
||||
print(f"### shuffling narrators")
|
||||
random.shuffle(self._narrators)
|
||||
self._i = 0
|
||||
|
||||
self._narrator["narrator"] = self._narrators[self._i]
|
||||
print(f"### new narrator is {self._narrator}")
|
||||
yield frame
|
||||
|
||||
|
||||
class VideoImageFrameProcessor(FrameProcessor):
|
||||
def __init__(self, narrator):
|
||||
self._narrator = narrator
|
||||
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, (VideoImageFrame, TelestratorImageFrame)):
|
||||
yield VisionFrame(self._narrator["narrator"]["prompt"], frame.image)
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class ImageRefresher(FrameProcessor):
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, LLMResponseEndFrame):
|
||||
yield RequestVideoImageFrame(participantId=None)
|
||||
yield frame
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
class TelestratorImageWrapper(FrameProcessor):
|
||||
async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]:
|
||||
if isinstance(frame, ImageFrame):
|
||||
yield TelestratorImageFrame(None, frame.image)
|
||||
else:
|
||||
yield frame
|
||||
|
||||
|
||||
async def main(room_url: str, token):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
transport = DailyTransportService(
|
||||
room_url,
|
||||
token,
|
||||
"Respond bot",
|
||||
duration_minutes=5,
|
||||
start_transcription=True,
|
||||
mic_enabled=True,
|
||||
mic_sample_rate=16000,
|
||||
camera_enabled=True,
|
||||
camera_width=1024,
|
||||
camera_height=1024,
|
||||
vad_enabled=False,
|
||||
receive_video=True,
|
||||
receive_video_fps=0
|
||||
)
|
||||
|
||||
tts = ElevenLabsTTSService(
|
||||
aiohttp_session=session,
|
||||
api_key=os.getenv("ELEVENLABS_API_KEY"),
|
||||
narrator=narrator,
|
||||
aggregate_sentences=False
|
||||
)
|
||||
|
||||
llm = OpenAILLMService(
|
||||
api_key=os.getenv("OPENAI_CHATGPT_API_KEY"),
|
||||
model="gpt-4-turbo-preview")
|
||||
|
||||
vs = OpenAIVisionService(api_key=os.getenv("OPENAI_CHATGPT_API_KEY"))
|
||||
vifp = VideoImageFrameProcessor(narrator)
|
||||
ir = ImageRefresher()
|
||||
img = FalImageGenService(
|
||||
image_size="1024x1024",
|
||||
aiohttp_session=session,
|
||||
key_id=os.getenv("FAL_KEY_ID"),
|
||||
key_secret=os.getenv("FAL_KEY_SECRET"),
|
||||
)
|
||||
tiw = TelestratorImageWrapper()
|
||||
lfra = LLMFullResponseAggregator()
|
||||
lfra1 = LLMFullResponseAggregator()
|
||||
lfra2 = LLMFullResponseAggregator()
|
||||
lfra3 = LLMFullResponseAggregator()
|
||||
lfra4 = LLMFullResponseAggregator()
|
||||
fl0 = FrameLogger("@@@ About to describe")
|
||||
fl1 = FrameLogger("!!! About to image gen")
|
||||
f4 = FrameLogger("((( partway through )))")
|
||||
f5 = FrameLogger("!!! f5")
|
||||
ns = NarratorShuffle(narrator, narrators)
|
||||
t1 = TranslationProcessor("English", "Spanish")
|
||||
t2 = TranslationProcessor("Spanish", "German")
|
||||
t3 = TranslationProcessor("German", "Japanese")
|
||||
t4 = TranslationProcessor("Japanese", "English")
|
||||
pipeline = Pipeline(
|
||||
processors=[
|
||||
fl0,
|
||||
vifp,
|
||||
vs,
|
||||
lfra,
|
||||
tts,
|
||||
fl1,
|
||||
img,
|
||||
tiw,
|
||||
],
|
||||
)
|
||||
|
||||
@transport.event_handler("on_first_other_participant_joined")
|
||||
async def on_first_other_participant_joined(transport):
|
||||
await pipeline.queue_frames([RequestVideoImageFrame(participantId=None)])
|
||||
|
||||
transport.transcription_settings["extra"]["endpointing"] = True
|
||||
transport.transcription_settings["extra"]["punctuate"] = True
|
||||
await transport.run(pipeline)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
(url, token) = configure()
|
||||
asyncio.run(main(url, token))
|
||||