lucy1118 commited on
Commit
efc9173
·
verified ·
1 Parent(s): 2998665

Upload 53 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +3 -0
  2. .gitignore +158 -0
  3. Dockerfile +52 -3
  4. env.example +9 -0
  5. frontend/.eslintrc.json +3 -0
  6. frontend/.gitignore +36 -0
  7. frontend/README.md +36 -0
  8. frontend/app/favicon.ico +0 -0
  9. frontend/app/globals.css +109 -0
  10. frontend/app/layout.tsx +46 -0
  11. frontend/app/opengraph-image.png +3 -0
  12. frontend/app/page.tsx +16 -0
  13. frontend/app/twitter-image.png +3 -0
  14. frontend/app/utils.ts +6 -0
  15. frontend/components.json +17 -0
  16. frontend/components/App.tsx +81 -0
  17. frontend/components/AudioIndicator/index.tsx +69 -0
  18. frontend/components/DevicePicker/index.tsx +139 -0
  19. frontend/components/MicToggle/index.tsx +34 -0
  20. frontend/components/Setup.tsx +94 -0
  21. frontend/components/Story.tsx +79 -0
  22. frontend/components/StoryTranscript/StoryTranscript.module.css +42 -0
  23. frontend/components/StoryTranscript/index.tsx +55 -0
  24. frontend/components/UserInputIndicator/UserInputIndicator.module.css +82 -0
  25. frontend/components/UserInputIndicator/index.tsx +48 -0
  26. frontend/components/VideoTile/VideoTile.module.css +34 -0
  27. frontend/components/VideoTile/index.tsx +27 -0
  28. frontend/components/WaveText/WaveText.module.css +68 -0
  29. frontend/components/WaveText/index.tsx +23 -0
  30. frontend/components/ui/button.tsx +56 -0
  31. frontend/components/ui/select.tsx +160 -0
  32. frontend/components/ui/typewriter.tsx +61 -0
  33. frontend/env.example +1 -0
  34. frontend/next.config.mjs +15 -0
  35. frontend/package.json +38 -0
  36. frontend/postcss.config.js +6 -0
  37. frontend/public/alpha-mask.gif +0 -0
  38. frontend/public/bg.jpg +0 -0
  39. frontend/tailwind.config.ts +86 -0
  40. frontend/tsconfig.json +40 -0
  41. frontend/yarn.lock +0 -0
  42. image.png +3 -0
  43. requirements.txt +6 -0
  44. src/assets/book1.png +0 -0
  45. src/assets/book2.png +0 -0
  46. src/assets/ding.wav +0 -0
  47. src/assets/listening.wav +0 -0
  48. src/assets/talking.wav +0 -0
  49. src/bot.py +161 -0
  50. src/bot_runner.py +233 -0
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ frontend/app/opengraph-image.png filter=lfs diff=lfs merge=lfs -text
37
+ frontend/app/twitter-image.png filter=lfs diff=lfs merge=lfs -text
38
+ image.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ .idea/
3
+
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+ .DS_Store
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ #poetry.lock
107
+
108
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
109
+ __pypackages__/
110
+
111
+ # Celery stuff
112
+ celerybeat-schedule
113
+ celerybeat.pid
114
+
115
+ # SageMath parsed files
116
+ *.sage.py
117
+
118
+ # Environments
119
+ .env
120
+ .venv
121
+ env/
122
+ venv/
123
+ ENV/
124
+ env.bak/
125
+ venv.bak/
126
+
127
+ # Spyder project settings
128
+ .spyderproject
129
+ .spyproject
130
+
131
+ # Rope project settings
132
+ .ropeproject
133
+
134
+ # mkdocs documentation
135
+ /site
136
+
137
+ # mypy
138
+ .mypy_cache/
139
+ .dmypy.json
140
+ dmypy.json
141
+
142
+ # Pyre type checker
143
+ .pyre/
144
+
145
+ # pytype static type analyzer
146
+ .pytype/
147
+
148
+ # Cython debug symbols
149
+ cython_debug/
150
+
151
+ # PyCharm
152
+ # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
153
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
154
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
155
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
156
+ #.idea/
157
+
158
+ read.html
Dockerfile CHANGED
@@ -1,5 +1,54 @@
1
- From ghcr.io/gordonchanfz/storytelling:latest
2
- WORKDIR /app
 
 
 
 
 
 
3
  ENV FAST_API_PORT=7860
4
  EXPOSE 7860
5
- CMD python3 src/server.py --port ${FAST_API_PORT}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim-bookworm
2
+
3
+ ARG DEBIAN_FRONTEND=noninteractive
4
+ ARG USE_PERSISTENT_DATA
5
+ ENV PYTHONUNBUFFERED=1
6
+ ENV NODE_MAJOR=20
7
+
8
+ # Expose FastAPI port
9
  ENV FAST_API_PORT=7860
10
  EXPOSE 7860
11
+
12
+ # Install system dependencies
13
+ RUN apt-get update && apt-get install --no-install-recommends -y \
14
+ build-essential \
15
+ git \
16
+ ffmpeg \
17
+ google-perftools \
18
+ ca-certificates curl gnupg \
19
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
20
+
21
+ # Install Node.js
22
+ RUN mkdir -p /etc/apt/keyrings
23
+ RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
24
+ RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null
25
+ RUN apt-get update && apt-get install nodejs -y
26
+
27
+ # Set up a new user named "user" with user ID 1000
28
+ RUN useradd -m -u 1000 user
29
+
30
+ # Set home to the user's home directory
31
+ ENV HOME=/home/user \
32
+ PATH=/home/user/.local/bin:$PATH \
33
+ PYTHONPATH=$HOME/app \
34
+ PYTHONUNBUFFERED=1
35
+
36
+ # Switch to the "user" user
37
+ USER user
38
+
39
+ # Set the working directory to the user's home directory
40
+ WORKDIR $HOME/app
41
+
42
+ # Install Python dependencies
43
+ COPY ./requirements.txt requirements.txt
44
+ RUN pip3 install --no-cache-dir --upgrade -r requirements.txt
45
+
46
+ # Copy everything else
47
+ COPY --chown=user ./src/ src/
48
+
49
+ # Copy frontend app and build
50
+ COPY --chown=user ./frontend/ frontend/
51
+ RUN cd frontend && npm install && npm run build
52
+
53
+ # Start the FastAPI server
54
+ CMD python3 src/bot_runner.py --port ${FAST_API_PORT}
env.example ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ DAILY_API_KEY=
2
+ DAILY_SAMPLE_ROOM_URL=
3
+ ELEVENLABS_API_KEY=
4
+ ELEVENLABS_VOICE_ID=
5
+ FAL_KEY=
6
+ OPENAI_API_KEY=
7
+
8
+ ENV= # dev | production
9
+ RUN_AS_VM= # Set this if you want to run bots on process (not launch a new VM)
frontend/.eslintrc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "extends": "next/core-web-vitals"
3
+ }
frontend/.gitignore ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+ .yarn/install-state.gz
8
+
9
+ # testing
10
+ /coverage
11
+
12
+ # next.js
13
+ /.next/
14
+ /out/
15
+
16
+ # production
17
+ /build
18
+
19
+ # misc
20
+ .DS_Store
21
+ *.pem
22
+
23
+ # debug
24
+ npm-debug.log*
25
+ yarn-debug.log*
26
+ yarn-error.log*
27
+
28
+ # local env files
29
+ .env*.local
30
+
31
+ # vercel
32
+ .vercel
33
+
34
+ # typescript
35
+ *.tsbuildinfo
36
+ next-env.d.ts
frontend/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
frontend/app/favicon.ico ADDED
frontend/app/globals.css ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 224 71.4% 4.1%;
9
+
10
+ --card: 0 0% 100%;
11
+ --card-foreground: 224 71.4% 4.1%;
12
+
13
+ --popover: 0 0% 100%;
14
+ --popover-foreground: 224 71.4% 4.1%;
15
+
16
+ --primary: 220.9 39.3% 11%;
17
+ --primary-foreground: 210 20% 98%;
18
+
19
+ --secondary: 220 14.3% 95.9%;
20
+ --secondary-foreground: 220.9 39.3% 11%;
21
+
22
+ --muted: 220 14.3% 95.9%;
23
+ --muted-foreground: 220 8.9% 46.1%;
24
+
25
+ --accent: 220 14.3% 95.9%;
26
+ --accent-foreground: 220.9 39.3% 11%;
27
+
28
+ --destructive: 0 84.2% 60.2%;
29
+ --destructive-foreground: 210 20% 98%;
30
+
31
+ --border: 220 13% 91%;
32
+ --input: 220 13% 91%;
33
+ --ring: 224 71.4% 4.1%;
34
+
35
+ --radius: 0.5rem;
36
+ }
37
+
38
+ .dark {
39
+ --background: 224 71.4% 4.1%;
40
+ --foreground: 210 20% 98%;
41
+
42
+ --card: 224 71.4% 4.1%;
43
+ --card-foreground: 210 20% 98%;
44
+
45
+ --popover: 224 71.4% 4.1%;
46
+ --popover-foreground: 210 20% 98%;
47
+
48
+ --primary: 210 20% 98%;
49
+ --primary-foreground: 220.9 39.3% 11%;
50
+
51
+ --secondary: 215 27.9% 16.9%;
52
+ --secondary-foreground: 210 20% 98%;
53
+
54
+ --muted: 215 27.9% 16.9%;
55
+ --muted-foreground: 217.9 10.6% 64.9%;
56
+
57
+ --accent: 215 27.9% 16.9%;
58
+ --accent-foreground: 210 20% 98%;
59
+
60
+ --destructive: 0 62.8% 30.6%;
61
+ --destructive-foreground: 210 20% 98%;
62
+
63
+ --border: 215 27.9% 16.9%;
64
+ --input: 215 27.9% 16.9%;
65
+ --ring: 216 12.2% 83.9%;
66
+ }
67
+ }
68
+
69
+ @layer base {
70
+ * {
71
+ @apply border-border;
72
+ }
73
+ body {
74
+ @apply bg-background text-foreground;
75
+ }
76
+ }
77
+
78
+ body{
79
+ background: url("/bg.jpg") no-repeat center center;
80
+ background-size: cover;
81
+ }
82
+
83
+ .cardShadow{
84
+ box-shadow: 0px 124px 35px 0px rgba(0, 0, 0, 0.00), 0px 79px 32px 0px rgba(0, 0, 0, 0.01), 0px 45px 27px 0px rgba(0, 0, 0, 0.05), 0px 20px 20px 0px rgba(0, 0, 0, 0.09), 0px 5px 11px 0px rgba(0, 0, 0, 0.10);
85
+ }
86
+
87
+ @keyframes fadeInSlideUp {
88
+ 0% {
89
+ opacity: 0;
90
+ transform: translateY(50px);
91
+ }
92
+ 100% {
93
+ opacity: 1;
94
+ transform: translateY(0);
95
+ }
96
+ }
97
+
98
+ .fade-in {
99
+ animation: fadeIn 1s ease-out;
100
+ }
101
+
102
+ @keyframes fadeIn {
103
+ 0% {
104
+ opacity: 0;
105
+ }
106
+ 100% {
107
+ opacity: 1;
108
+ }
109
+ }
frontend/app/layout.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ import "./globals.css";
4
+ import type { Metadata } from "next";
5
+ import { Space_Grotesk, Space_Mono } from "next/font/google";
6
+
7
+ import { cn } from "@/app/utils";
8
+
9
+ // Font
10
+ const sans = Space_Grotesk({
11
+ subsets: ["latin"],
12
+ weight: ["400", "500", "600"],
13
+ variable: "--font-sans",
14
+ });
15
+
16
+ const mono = Space_Mono({
17
+ subsets: ["latin"],
18
+ weight: ["400", "700"],
19
+ variable: "--font-mono",
20
+ });
21
+
22
+ export const metadata: Metadata = {
23
+ title: "Storytelling Chatbot - Daily AI",
24
+ description: "Built with git.new/ai",
25
+ metadataBase: new URL(process.env.SITE_URL || "http://localhost:3000"),
26
+ };
27
+
28
+ export default function RootLayout({
29
+ children,
30
+ }: Readonly<{
31
+ children: React.ReactNode;
32
+ }>) {
33
+ return (
34
+ <html lang="en">
35
+ <body
36
+ className={cn(
37
+ "min-h-screen bg-background font-sans antialiased flex flex-col",
38
+ sans.variable,
39
+ mono.variable
40
+ )}
41
+ >
42
+ <main className="flex flex-1">{children}</main>
43
+ </body>
44
+ </html>
45
+ );
46
+ }
frontend/app/opengraph-image.png ADDED

Git LFS Details

  • SHA256: 5ef16a20c8450b3404c334676d478488bda7ee2958a73dbada70d719e56d0cff
  • Pointer size: 132 Bytes
  • Size of remote file: 1.31 MB
frontend/app/page.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { DailyProvider, useCallObject } from "@daily-co/daily-react";
5
+
6
+ import App from "../components/App";
7
+
8
+ export default function Home() {
9
+ const callObject = useCallObject({});
10
+
11
+ return (
12
+ <DailyProvider callObject={callObject}>
13
+ <App />
14
+ </DailyProvider>
15
+ );
16
+ }
frontend/app/twitter-image.png ADDED

Git LFS Details

  • SHA256: 4c6e635eac7c5c30693c95d293a55ff9f825b1622a947441afdc11c82cb087cf
  • Pointer size: 132 Bytes
  • Size of remote file: 2.47 MB
frontend/app/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
frontend/components.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "app/globals.css",
9
+ "baseColor": "gray",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/app"
16
+ }
17
+ }
frontend/components/App.tsx ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+
5
+ import { useDaily } from "@daily-co/daily-react";
6
+ import Setup from "./Setup";
7
+ import Story from "./Story";
8
+
9
+ type State =
10
+ | "idle"
11
+ | "connecting"
12
+ | "connected"
13
+ | "started"
14
+ | "finished"
15
+ | "error";
16
+
17
+ export default function Call() {
18
+ const daily = useDaily();
19
+
20
+ const [state, setState] = useState<State>("idle");
21
+ const [room, setRoom] = useState<string | null>(null);
22
+
23
+ async function start() {
24
+ setState("connecting");
25
+
26
+ if (!daily) return;
27
+
28
+ // Create a new room for the story session
29
+ try {
30
+ const response = await fetch("/start_bot", {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ },
35
+ });
36
+
37
+ const { room_url, token } = await response.json();
38
+
39
+ // Keep a reference to the room url for later
40
+ setRoom(room_url);
41
+
42
+ // Join the WebRTC session
43
+ await daily.join({
44
+ url: room_url,
45
+ token,
46
+ videoSource: false,
47
+ startAudioOff: true,
48
+ });
49
+
50
+ setState("connected");
51
+
52
+ // Disable local audio, the bot will say hello first
53
+ daily.setLocalAudio(false);
54
+
55
+ setState("started");
56
+ } catch (error) {
57
+ setState("error");
58
+ }
59
+ }
60
+
61
+ async function leave() {
62
+ await daily?.leave();
63
+ setState("finished");
64
+ }
65
+
66
+ if (state === "error") {
67
+ return (
68
+ <div className="flex items-center mx-auto">
69
+ <p className="text-red-500 font-semibold bg-white px-4 py-2 shadow-xl rounded-lg">
70
+ This demo is currently at capacity. Please try again later.
71
+ </p>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ if (state === "started") {
77
+ return <Story handleLeave={() => leave()} />;
78
+ }
79
+
80
+ return <Setup handleStart={() => start()} />;
81
+ }
frontend/components/AudioIndicator/index.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ useAudioLevel,
3
+ useAudioTrack,
4
+ useLocalSessionId,
5
+ } from "@daily-co/daily-react";
6
+ import { useCallback, useRef } from "react";
7
+
8
+ export const AudioIndicator: React.FC = () => {
9
+ const localSessionId = useLocalSessionId();
10
+ const audioTrack = useAudioTrack(localSessionId);
11
+ const volRef = useRef<HTMLDivElement>(null);
12
+
13
+ useAudioLevel(
14
+ audioTrack?.persistentTrack,
15
+ useCallback((volume) => {
16
+ // this volume number will be between 0 and 1
17
+ // give it a minimum scale of 0.15 to not completely disappear 👻
18
+ if (volRef.current) {
19
+ const v = volume * 1.75;
20
+ volRef.current.style.transform = `scale(${Math.max(0.1, v)})`;
21
+ }
22
+ }, [])
23
+ );
24
+
25
+ // Your audio track's audio volume visualized in a small circle,
26
+ // whose size changes depending on the volume level
27
+ return (
28
+ <>
29
+ <div className="vol bg-teal-700" ref={volRef} />
30
+ <style jsx>{`
31
+ .vol {
32
+ position: absolute;
33
+ overflow: hidden;
34
+ inset: 0px;
35
+ z-index: 0;
36
+ border-radius: 999px;
37
+ transition: all 0.1s ease;
38
+ transform: scale(0);
39
+ }
40
+ `}</style>
41
+ </>
42
+ );
43
+ };
44
+
45
+ export const AudioIndicatorBar: React.FC = () => {
46
+ const localSessionId = useLocalSessionId();
47
+ const audioTrack = useAudioTrack(localSessionId);
48
+
49
+ const volRef = useRef<HTMLDivElement>(null);
50
+
51
+ useAudioLevel(
52
+ audioTrack?.persistentTrack,
53
+ useCallback((volume) => {
54
+ if (volRef.current)
55
+ volRef.current.style.width = Math.max(2, volume * 100) + "%";
56
+ }, [])
57
+ );
58
+
59
+ return (
60
+ <div className="flex-1 bg-gray-200 h-[8px] rounded-full overflow-hidden">
61
+ <div
62
+ className="bg-green-500 h-[8px] w-[0] rounded-full transition-all duration-100 ease"
63
+ ref={volRef}
64
+ />
65
+ </div>
66
+ );
67
+ };
68
+
69
+ export default AudioIndicator;
frontend/components/DevicePicker/index.tsx ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { DailyMeetingState } from "@daily-co/daily-js";
5
+ import { useDaily, useDevices } from "@daily-co/daily-react";
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from "@/components/ui/select";
13
+ import { IconMicrophone, IconDeviceSpeaker } from "@tabler/icons-react";
14
+ import { AudioIndicatorBar } from "../AudioIndicator";
15
+
16
+ interface Props {}
17
+
18
+ export default function DevicePicker({}: Props) {
19
+ const daily = useDaily();
20
+ const {
21
+ currentMic,
22
+ hasMicError,
23
+ micState,
24
+ microphones,
25
+ setMicrophone,
26
+ currentSpeaker,
27
+ speakers,
28
+ setSpeaker,
29
+ } = useDevices();
30
+
31
+ const handleMicrophoneChange = (value: string) => {
32
+ setMicrophone(value);
33
+ };
34
+
35
+ const handleSpeakerChange = (value: string) => {
36
+ setSpeaker(value);
37
+ };
38
+
39
+ useEffect(() => {
40
+ if (microphones.length > 0 || !daily || daily.isDestroyed()) return;
41
+ const meetingState = daily.meetingState();
42
+ const meetingStatesBeforeJoin: DailyMeetingState[] = [
43
+ "new",
44
+ "loading",
45
+ "loaded",
46
+ ];
47
+ if (meetingStatesBeforeJoin.includes(meetingState)) {
48
+ daily.startCamera({ startVideoOff: true, startAudioOff: false });
49
+ }
50
+ }, [daily, microphones]);
51
+
52
+ return (
53
+ <div className="flex flex-col gap-5">
54
+ <section>
55
+ <label className="uppercase text-sm tracking-wider text-gray-500">
56
+ Microphone:
57
+ </label>
58
+ <div className="flex flex-row gap-4 items-center mt-2">
59
+ <IconMicrophone size={24} />
60
+ <div className="flex flex-col flex-1 gap-3">
61
+ <Select onValueChange={handleMicrophoneChange}>
62
+ <SelectTrigger className="">
63
+ <SelectValue
64
+ placeholder={
65
+ hasMicError ? "error" : currentMic?.device?.label
66
+ }
67
+ />
68
+ </SelectTrigger>
69
+ <SelectContent>
70
+ {hasMicError && (
71
+ <option value="error" disabled>
72
+ No microphone access.
73
+ </option>
74
+ )}
75
+
76
+ {microphones.map((m) => (
77
+ <SelectItem key={m.device.deviceId} value={m.device.deviceId}>
78
+ {m.device.label}
79
+ </SelectItem>
80
+ ))}
81
+ </SelectContent>
82
+ </Select>
83
+ <AudioIndicatorBar />
84
+ </div>
85
+ </div>
86
+ </section>
87
+
88
+ <section>
89
+ <label className="uppercase text-sm tracking-wider text-gray-500">
90
+ Speakers:
91
+ </label>
92
+ <div className="flex flex-row gap-4 items-center mt-2">
93
+ <IconDeviceSpeaker size={24} />
94
+ <Select onValueChange={handleSpeakerChange}>
95
+ <SelectTrigger className="">
96
+ <SelectValue placeholder={currentSpeaker?.device?.label} />
97
+ </SelectTrigger>
98
+ <SelectContent>
99
+ {speakers.map((m) => (
100
+ <SelectItem key={m.device.deviceId} value={m.device.deviceId}>
101
+ {m.device.label}
102
+ </SelectItem>
103
+ ))}
104
+ </SelectContent>
105
+ </Select>
106
+ </div>
107
+ </section>
108
+ {hasMicError && (
109
+ <div className="error">
110
+ {micState === "blocked" ? (
111
+ <p className="text-red-500">
112
+ Please check your browser and system permissions. Make sure that
113
+ this app is allowed to access your microphone.
114
+ </p>
115
+ ) : micState === "in-use" ? (
116
+ <p className="text-red-500">
117
+ Your microphone is being used by another app. Please close any
118
+ other apps using your microphone and restart this app.
119
+ </p>
120
+ ) : micState === "not-found" ? (
121
+ <p className="text-red-500">
122
+ No microphone seems to be connected. Please connect a microphone.
123
+ </p>
124
+ ) : micState === "not-supported" ? (
125
+ <p className="text-red-500">
126
+ This app is not supported on your device. Please update your
127
+ software or use a different device.
128
+ </p>
129
+ ) : (
130
+ <p className="text-red-500">
131
+ There seems to be an issue accessing your microphone. Try
132
+ restarting the app or consult a system administrator.
133
+ </p>
134
+ )}
135
+ </div>
136
+ )}
137
+ </div>
138
+ );
139
+ }
frontend/components/MicToggle/index.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ useDaily,
3
+ useLocalSessionId,
4
+ useMediaTrack,
5
+ } from "@daily-co/daily-react";
6
+ import { useCallback } from "react";
7
+ import { IconMicrophone, IconMicrophoneOff } from "@tabler/icons-react";
8
+
9
+ export const MicToggle: React.FC = () => {
10
+ const daily = useDaily();
11
+ const localSessionId = useLocalSessionId();
12
+ const audioTrack = useMediaTrack(localSessionId, "audio");
13
+ const isMicMuted =
14
+ audioTrack.state === "blocked" || audioTrack.state === "off";
15
+
16
+ const handleClick = useCallback(() => {
17
+ if (!daily) return;
18
+ daily.setLocalAudio(isMicMuted);
19
+ }, [daily, isMicMuted]);
20
+
21
+ const text = isMicMuted ? (
22
+ <IconMicrophone size={21} />
23
+ ) : (
24
+ <IconMicrophoneOff size={21} />
25
+ );
26
+
27
+ return (
28
+ <button className="MicToggle UIButton" onClick={handleClick}>
29
+ {text}
30
+ </button>
31
+ );
32
+ };
33
+
34
+ export default MicToggle;
frontend/components/Setup.tsx ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import DevicePicker from "@/components/DevicePicker";
4
+ import { IconAlertCircle, IconEar, IconLoader2 } from "@tabler/icons-react";
5
+
6
+ type SetupProps = {
7
+ handleStart: () => void;
8
+ };
9
+
10
+ const buttonLabel = {
11
+ intro: "Next",
12
+ setup: "Let's begin!",
13
+ loading: "Joining...",
14
+ };
15
+ export const Setup: React.FC<SetupProps> = ({ handleStart }) => {
16
+ const [state, setState] = React.useState<"intro" | "setup" | "loading">(
17
+ "intro"
18
+ );
19
+
20
+ return (
21
+ <div className="w-full flex flex-col items-center justify-between">
22
+ <div className="bg-white rounded-3xl cardAnim cardShadow p-9 max-w-screen-sm mx-auto outline outline-[5px] outline-gray-600/10 my-auto">
23
+ <div className="flex flex-col gap-6">
24
+ <h1 className="text-4xl font-bold text-pretty tracking-tighter mb-4">
25
+ Welcome to <span className="text-sky-500">Storytime</span>
26
+ </h1>
27
+ {state === "intro" ? (
28
+ <>
29
+ <p className="text-gray-600 leading-relaxed text-pretty">
30
+ This app demos a voice-controlled storytelling chatbot. It will
31
+ start with the bot asking you what kind of story you&apos;d like
32
+ to hear (e.g. a fairy tale, a mystery, etc.). After each scene,
33
+ the bot will pause to ask for your input. Direct the story any
34
+ way you choose!
35
+ </p>
36
+ <p className="flex flex-row gap-2 text-gray-600 font-medium">
37
+ <IconEar size={24} /> For best results, try in a quiet
38
+ environment!
39
+ </p>
40
+ <p className="flex flex-row gap-2 text-gray-600 font-medium text-red-500">
41
+ <IconAlertCircle size={24} /> This demo expires after 5 minutes.
42
+ </p>
43
+ </>
44
+ ) : (
45
+ <>
46
+ <p className="text-gray-600 leading-relaxed text-pretty">
47
+ Since you&apos;ll be talking to Storybot, we need to make sure
48
+ it can hear you! Please configure your microphone and speakers
49
+ below.
50
+ </p>
51
+ <DevicePicker />
52
+ </>
53
+ )}
54
+ <hr className="border-gray-150 my-2" />
55
+
56
+ <Button
57
+ size="lg"
58
+ disabled={state === "loading"}
59
+ onClick={() => {
60
+ if (state === "intro") {
61
+ setState("setup");
62
+ } else {
63
+ setState("loading");
64
+ handleStart();
65
+ }
66
+ }}
67
+ >
68
+ {state === "loading" && (
69
+ <IconLoader2
70
+ size={21}
71
+ stroke={2}
72
+ className="mr-2 h-4 w-4 animate-spin"
73
+ />
74
+ )}
75
+ {buttonLabel[state]}
76
+ </Button>
77
+ </div>
78
+ </div>
79
+ <footer className="flex-0 text-center font-mono text-sm text-gray-100 py-6">
80
+ <span className="bg-gray-800/70 px-3 py-1 rounded-md">
81
+ Created with{" "}
82
+ <a
83
+ href="https://git.new/ai"
84
+ className="text-violet-300 underline decoration-violet-400 hover:text-violet-100"
85
+ >
86
+ git.new/ai
87
+ </a>
88
+ </span>
89
+ </footer>
90
+ </div>
91
+ );
92
+ };
93
+
94
+ export default Setup;
frontend/components/Story.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import {
3
+ useDaily,
4
+ useParticipantIds,
5
+ useAppMessage,
6
+ DailyAudio,
7
+ } from "@daily-co/daily-react";
8
+ import { IconLogout, IconLoader2 } from "@tabler/icons-react";
9
+
10
+ import VideoTile from "@/components/VideoTile";
11
+ import { Button } from "@/components/ui/button";
12
+ import UserInputIndicator from "@/components/UserInputIndicator";
13
+ import WaveText from "@/components/WaveText";
14
+
15
+ interface StoryProps {
16
+ handleLeave: () => void;
17
+ }
18
+
19
+ const Story: React.FC<StoryProps> = ({ handleLeave }) => {
20
+ const daily = useDaily();
21
+ const participantIds = useParticipantIds({ filter: "remote" });
22
+ const [storyState, setStoryState] = useState<"user" | "assistant">(
23
+ "assistant"
24
+ );
25
+
26
+ useAppMessage({
27
+ onAppMessage: (e) => {
28
+ if (!daily || !e.data?.cue) return;
29
+
30
+ // Determine the UI state from the cue sent by the bot
31
+ if (e.data?.cue === "user_turn") {
32
+ // Delay enabling local mic input to avoid feedback from LLM
33
+ setTimeout(() => daily.setLocalAudio(true), 500);
34
+ setStoryState("user");
35
+ } else {
36
+ daily.setLocalAudio(false);
37
+ setStoryState("assistant");
38
+ }
39
+ },
40
+ });
41
+
42
+ return (
43
+ <div className="w-full flex flex-col flex-1 self-stretch">
44
+ {/* Absolute elements */}
45
+ <div className="absolute top-20 w-full text-center z-50">
46
+ <WaveText active={storyState === "user"} />
47
+ </div>
48
+ <header className="flex absolute top-0 w-full z-50 p-6 justify-end">
49
+ <Button variant="secondary" onClick={() => handleLeave()}>
50
+ <IconLogout size={21} className="mr-2" />
51
+ Exit
52
+ </Button>
53
+ </header>
54
+ <div className="absolute inset-0 bg-gray-800 bg-opacity-90 z-10 fade-in"></div>
55
+
56
+ {/* Static elements */}
57
+ <div className="relative z-20 flex-1 flex items-center justify-center">
58
+ {participantIds.length >= 1 ? (
59
+ <VideoTile
60
+ sessionId={participantIds[0]}
61
+ inactive={storyState === "user"}
62
+ />
63
+ ) : (
64
+ <span className="p-3 rounded-full bg-gray-900/60 animate-pulse">
65
+ <IconLoader2
66
+ size={42}
67
+ stroke={2}
68
+ className="animate-spin text-white z-20 self-center"
69
+ />
70
+ </span>
71
+ )}
72
+ <DailyAudio />
73
+ </div>
74
+ <UserInputIndicator active={storyState === "user"} />
75
+ </div>
76
+ );
77
+ };
78
+
79
+ export default Story;
frontend/components/StoryTranscript/StoryTranscript.module.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .container{
2
+ position: absolute;
3
+ color:white;
4
+ z-index: 50;
5
+ margin: 0 auto;
6
+ display: flex;
7
+ flex-direction: column;
8
+ @apply gap-4 inset-6;
9
+ align-items: center;
10
+ justify-content: end;
11
+ text-align: center;
12
+ }
13
+
14
+ .transcript{
15
+ @apply font-semibold;
16
+ }
17
+
18
+ .transcript span{
19
+ box-decoration-break: clone;
20
+ @apply bg-gray-900/80 rounded-md px-4 py-2;
21
+ }
22
+
23
+ .sentence{
24
+ opacity: 1;
25
+ animation: fadeOut 2.5s linear forwards;
26
+ animation-delay: 1s;
27
+ }
28
+ @keyframes fadeOut {
29
+ 0% {
30
+ opacity: 1;
31
+ transform: scale(1);
32
+ }
33
+ 20% {
34
+ transform: scale(1);
35
+ filter: blur(0);
36
+ }
37
+ 100% {
38
+ transform: scale(0.8) translateY(-50%);
39
+ filter: blur(25px);
40
+ opacity: 0;
41
+ }
42
+ }
frontend/components/StoryTranscript/index.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useEffect, useRef, useState } from "react";
4
+ import { useAppMessage } from "@daily-co/daily-react";
5
+ import { DailyEventObjectAppMessage } from "@daily-co/daily-js";
6
+
7
+ import styles from "./StoryTranscript.module.css";
8
+
9
+ export default function StoryTranscript() {
10
+ const [partialText, setPartialText] = useState<string>("");
11
+ const [sentences, setSentences] = useState<string[]>([]);
12
+ const intervalRef = useRef<any | null>(null);
13
+
14
+ useEffect(() => {
15
+ clearInterval(intervalRef.current);
16
+
17
+ intervalRef.current = setInterval(() => {
18
+ if (sentences.length > 2) {
19
+ setSentences((s) => s.slice(1));
20
+ }
21
+ }, 2500);
22
+
23
+ return () => clearInterval(intervalRef.current);
24
+ }, [sentences]);
25
+
26
+ useAppMessage({
27
+ onAppMessage: (e: DailyEventObjectAppMessage<any>) => {
28
+ if (e.fromId && e.fromId === "transcription") {
29
+ // Check for LLM transcripts only
30
+ if (e.data.user_id !== "") {
31
+ setPartialText(e.data.text);
32
+ if (e.data.is_final) {
33
+ setPartialText("");
34
+ setSentences((s) => [...s, e.data.text]);
35
+ }
36
+ }
37
+ }
38
+ },
39
+ });
40
+
41
+ return (
42
+ <div className={styles.container}>
43
+ {sentences.map((sentence, index) => (
44
+ <p key={index} className={`${styles.transcript} ${styles.sentence}`}>
45
+ <span>{sentence}</span>
46
+ </p>
47
+ ))}
48
+ {partialText && (
49
+ <p className={`${styles.transcript}`}>
50
+ <span>{partialText}</span>
51
+ </p>
52
+ )}
53
+ </div>
54
+ );
55
+ }
frontend/components/UserInputIndicator/UserInputIndicator.module.css ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .panel{
2
+ width: 100%;
3
+ pointer-events: none;
4
+ color: #ffffff;
5
+ text-align: center;
6
+ position: relative;
7
+ @apply pb-8;
8
+ }
9
+
10
+ .panel::after{
11
+ content: "";
12
+ position: absolute;
13
+ inset: 0px;
14
+ z-index: 10;
15
+ opacity: 0;
16
+ transition: all 0.3s ease;
17
+ @apply bg-gradient-to-t from-gray-950 to-transparent;
18
+ }
19
+
20
+ .active::after{ opacity: 1;}
21
+
22
+ .micIcon{
23
+ position: relative;
24
+ width: 120px;
25
+ height: 120px;
26
+ border-radius: 120px;
27
+ border: 6px solid;
28
+ outline: 6px solid;
29
+ display: flex;
30
+ justify-content: center;
31
+ align-items: center;
32
+ margin: 0 auto;
33
+ z-index: 20;
34
+ transition: all 0.5s ease;
35
+ @apply bg-gray-700/60 border-gray-600 outline-gray-900/20;
36
+
37
+ }
38
+
39
+ .micIcon svg{
40
+ position: relative;
41
+ z-index: 20;
42
+ opacity: 0.25;
43
+ transition: opacity 0.5s ease;
44
+ }
45
+
46
+
47
+ @keyframes pulse {
48
+ 0% {
49
+ outline-width: 6px;
50
+ @apply outline-teal-500/10;
51
+ }
52
+ 50% {
53
+ outline-width: 24px;
54
+ @apply outline-teal-500/50;
55
+ }
56
+ 100% {
57
+ outline-width: 6px;
58
+ @apply outline-teal-500/10;
59
+ }
60
+ }
61
+
62
+ .micIconActive{
63
+ @apply bg-teal-950 border-teal-500 outline-teal-500/20;
64
+ animation: pulse 2s infinite ease-in-out;
65
+ }
66
+
67
+ .micIconActive svg{
68
+ opacity: 1;
69
+ }
70
+
71
+ .transcript{
72
+ flex: 0;
73
+ align-self: center;
74
+ opacity: 0.25;
75
+ transition: opacity 1s ease;
76
+ transition-delay: 2.5s;
77
+ @apply bg-gray-900/90 font-medium py-1 px-2 rounded-sm mt-4;
78
+ }
79
+
80
+ .active .transcript{
81
+ opacity: 1;
82
+ }
frontend/components/UserInputIndicator/index.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from "react";
2
+
3
+ import { useAppMessage } from "@daily-co/daily-react";
4
+ import { DailyEventObjectAppMessage } from "@daily-co/daily-js";
5
+ import styles from "./UserInputIndicator.module.css";
6
+ import { IconMicrophone } from "@tabler/icons-react";
7
+ import { TypewriterEffect } from "../ui/typewriter";
8
+ import AudioIndicator from "../AudioIndicator";
9
+
10
+ interface Props {
11
+ active: boolean;
12
+ }
13
+
14
+ export default function UserInputIndicator({ active }: Props) {
15
+ const [transcription, setTranscription] = useState<string[]>([]);
16
+
17
+ useAppMessage({
18
+ onAppMessage: (e: DailyEventObjectAppMessage<any>) => {
19
+ if (e.fromId && e.fromId === "transcription") {
20
+ if (e.data.user_id === "" && e.data.is_final) {
21
+ setTranscription((t) => [...t, ...e.data.text.split(" ")]);
22
+ }
23
+ }
24
+ },
25
+ });
26
+
27
+ useEffect(() => {
28
+ if (active) return;
29
+ const t = setTimeout(() => setTranscription([]), 4000);
30
+ return () => clearTimeout(t);
31
+ }, [active]);
32
+
33
+ return (
34
+ <div className={`${styles.panel} ${active ? styles.active : ""}`}>
35
+ <div className="relative z-20 flex flex-col">
36
+ <div
37
+ className={`${styles.micIcon} ${active ? styles.micIconActive : ""}`}
38
+ >
39
+ <IconMicrophone size={42} />
40
+ {active && <AudioIndicator />}
41
+ </div>
42
+ <footer className={styles.transcript}>
43
+ <TypewriterEffect words={transcription} />
44
+ </footer>
45
+ </div>
46
+ </div>
47
+ );
48
+ }
frontend/components/VideoTile/VideoTile.module.css ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .container{
2
+ position: relative;
3
+ animation: fadeIn 3s ease;
4
+ transition: all 3s ease-out;
5
+ }
6
+
7
+ .videoTile{
8
+ @apply bg-gray-950;
9
+ width: 560px;
10
+ height: 560px;
11
+ mask-image: url('/alpha-mask.gif');
12
+ mask-size: cover;
13
+ mask-repeat: no-repeat;
14
+ margin:0 auto;
15
+ z-index: 10;
16
+ position: relative;
17
+ }
18
+
19
+ .inactive{
20
+ opacity: 0.7;
21
+ filter:blur(3px);
22
+ transform: scale(0.95)
23
+ }
24
+
25
+ @keyframes fadeIn {
26
+ 0% {
27
+ filter: blur(100px);
28
+ opacity: 0;
29
+ }
30
+ 100% {
31
+ filter: blur(0px);
32
+ opacity: 1;
33
+ }
34
+ }
frontend/components/VideoTile/index.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import styles from "./VideoTile.module.css";
3
+ import { DailyVideo } from "@daily-co/daily-react";
4
+ import StoryTranscript from "@/components/StoryTranscript";
5
+
6
+ interface Props {
7
+ sessionId: string;
8
+ inactive: boolean;
9
+ }
10
+
11
+ const VideoTile = ({ sessionId, inactive }: Props) => {
12
+ return (
13
+ <div className={`${styles.container} ${inactive ? styles.inactive : ""} `}>
14
+ <StoryTranscript />
15
+
16
+ <div className={styles.videoTile}>
17
+ <DailyVideo
18
+ sessionId={sessionId}
19
+ type={"video"}
20
+ className="aspect-square"
21
+ />
22
+ </div>
23
+ </div>
24
+ );
25
+ };
26
+
27
+ export default VideoTile;
frontend/components/WaveText/WaveText.module.css ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .waveText{
2
+ color: white;
3
+ text-shadow: 3px 3px 0px rgba(0,0,0,0.5);
4
+ opacity: 0;
5
+ transition: opacity 2s ease;
6
+ position: relative;
7
+ @apply text-4xl font-bold;
8
+ }
9
+
10
+ .active{
11
+ opacity: 1;
12
+ }
13
+
14
+ @keyframes jump {
15
+ 0% {
16
+ opacity: 0.5;
17
+ transform:translateY(0px)
18
+ }
19
+ 50% {
20
+ opacity: 1;
21
+ transform:translateY(-30px);
22
+ }
23
+ 100% {
24
+ opacity: 0.55;
25
+ transform:translateY(0px)
26
+ }
27
+ }
28
+
29
+ .waveText span{
30
+ display:inline-block;
31
+ animation:jump 2s infinite ease-in-out;
32
+
33
+ }
34
+
35
+ .waveText span:nth-child(1) {
36
+ animation-delay:0s;
37
+ }
38
+
39
+ .waveText span:nth-child(1) {
40
+ animation-delay:0.1s;
41
+ }
42
+ .waveText span:nth-child(2) {
43
+ animation-delay:0.2s;
44
+ }
45
+ .waveText span:nth-child(3) {
46
+ animation-delay:0.3s;
47
+ }
48
+ .waveText span:nth-child(4) {
49
+ animation-delay:0.4s;
50
+ }
51
+ .waveText span:nth-child(5) {
52
+ animation-delay:0.5s;
53
+ }
54
+ .waveText span:nth-child(6) {
55
+ animation-delay:0.6s;
56
+ }
57
+ .waveText span:nth-child(7) {
58
+ animation-delay:0.7s;
59
+ }
60
+ .waveText span:nth-child(8) {
61
+ animation-delay:0.8s;
62
+ }
63
+ .waveText span:nth-child(9) {
64
+ animation-delay:0.9s;
65
+ }
66
+ .waveText span:nth-child(10) {
67
+ animation-delay:1s;
68
+ }
frontend/components/WaveText/index.tsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import styles from "./WaveText.module.css";
3
+
4
+ interface Props {
5
+ active: boolean;
6
+ }
7
+
8
+ export default function WaveText({ active }: Props) {
9
+ return (
10
+ <div className={`${styles.waveText} ${active ? styles.active : ""}`}>
11
+ <span>W</span>
12
+ <span>h</span>
13
+ <span>a</span>
14
+ <span>t</span>
15
+ <span>&nbsp;&nbsp;</span>
16
+ <span>n</span>
17
+ <span>e</span>
18
+ <span>x</span>
19
+ <span>t</span>
20
+ <span>?</span>
21
+ </div>
22
+ );
23
+ }
frontend/components/ui/button.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/app/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-md font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-11 rounded-lg px-6 py-2",
24
+ sm: "h-9 rounded-md px-3",
25
+ lg: "h-12 rounded-xl px-8",
26
+ icon: "h-10 w-10",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ }
34
+ );
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean;
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button";
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+ );
54
+ Button.displayName = "Button";
55
+
56
+ export { Button, buttonVariants };
frontend/components/ui/select.tsx ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SelectPrimitive from "@radix-ui/react-select";
5
+ import { IconCheck, IconChevronDown, IconChevronUp } from "@tabler/icons-react";
6
+
7
+ import { cn } from "@/app/utils";
8
+
9
+ const Select = SelectPrimitive.Root;
10
+
11
+ const SelectGroup = SelectPrimitive.Group;
12
+
13
+ const SelectValue = SelectPrimitive.Value;
14
+
15
+ const SelectTrigger = React.forwardRef<
16
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
17
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
18
+ >(({ className, children, ...props }, ref) => (
19
+ <SelectPrimitive.Trigger
20
+ ref={ref}
21
+ className={cn(
22
+ "flex gap-3 h-12 w-full items-center justify-between rounded-xl border border-input bg-background px-4 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
23
+ className
24
+ )}
25
+ {...props}
26
+ >
27
+ {children}
28
+ <SelectPrimitive.Icon asChild>
29
+ <IconChevronDown className="h-4 w-4 opacity-50" />
30
+ </SelectPrimitive.Icon>
31
+ </SelectPrimitive.Trigger>
32
+ ));
33
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
34
+
35
+ const SelectScrollUpButton = React.forwardRef<
36
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
37
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
38
+ >(({ className, ...props }, ref) => (
39
+ <SelectPrimitive.ScrollUpButton
40
+ ref={ref}
41
+ className={cn(
42
+ "flex cursor-default items-center justify-center py-1",
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ <IconChevronUp className="h-4 w-4" />
48
+ </SelectPrimitive.ScrollUpButton>
49
+ ));
50
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
51
+
52
+ const SelectScrollDownButton = React.forwardRef<
53
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
54
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
55
+ >(({ className, ...props }, ref) => (
56
+ <SelectPrimitive.ScrollDownButton
57
+ ref={ref}
58
+ className={cn(
59
+ "flex cursor-default items-center justify-center py-1",
60
+ className
61
+ )}
62
+ {...props}
63
+ >
64
+ <IconChevronDown className="h-4 w-4" />
65
+ </SelectPrimitive.ScrollDownButton>
66
+ ));
67
+ SelectScrollDownButton.displayName =
68
+ SelectPrimitive.ScrollDownButton.displayName;
69
+
70
+ const SelectContent = React.forwardRef<
71
+ React.ElementRef<typeof SelectPrimitive.Content>,
72
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
73
+ >(({ className, children, position = "popper", ...props }, ref) => (
74
+ <SelectPrimitive.Portal>
75
+ <SelectPrimitive.Content
76
+ ref={ref}
77
+ className={cn(
78
+ "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
79
+ position === "popper" &&
80
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
81
+ className
82
+ )}
83
+ position={position}
84
+ {...props}
85
+ >
86
+ <SelectScrollUpButton />
87
+ <SelectPrimitive.Viewport
88
+ className={cn(
89
+ "p-1",
90
+ position === "popper" &&
91
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
92
+ )}
93
+ >
94
+ {children}
95
+ </SelectPrimitive.Viewport>
96
+ <SelectScrollDownButton />
97
+ </SelectPrimitive.Content>
98
+ </SelectPrimitive.Portal>
99
+ ));
100
+ SelectContent.displayName = SelectPrimitive.Content.displayName;
101
+
102
+ const SelectLabel = React.forwardRef<
103
+ React.ElementRef<typeof SelectPrimitive.Label>,
104
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
105
+ >(({ className, ...props }, ref) => (
106
+ <SelectPrimitive.Label
107
+ ref={ref}
108
+ className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
109
+ {...props}
110
+ />
111
+ ));
112
+ SelectLabel.displayName = SelectPrimitive.Label.displayName;
113
+
114
+ const SelectItem = React.forwardRef<
115
+ React.ElementRef<typeof SelectPrimitive.Item>,
116
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
117
+ >(({ className, children, ...props }, ref) => (
118
+ <SelectPrimitive.Item
119
+ ref={ref}
120
+ className={cn(
121
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
122
+ className
123
+ )}
124
+ {...props}
125
+ >
126
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
127
+ <SelectPrimitive.ItemIndicator>
128
+ <IconCheck className="h-4 w-4" />
129
+ </SelectPrimitive.ItemIndicator>
130
+ </span>
131
+
132
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
133
+ </SelectPrimitive.Item>
134
+ ));
135
+ SelectItem.displayName = SelectPrimitive.Item.displayName;
136
+
137
+ const SelectSeparator = React.forwardRef<
138
+ React.ElementRef<typeof SelectPrimitive.Separator>,
139
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
140
+ >(({ className, ...props }, ref) => (
141
+ <SelectPrimitive.Separator
142
+ ref={ref}
143
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
144
+ {...props}
145
+ />
146
+ ));
147
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
148
+
149
+ export {
150
+ Select,
151
+ SelectGroup,
152
+ SelectValue,
153
+ SelectTrigger,
154
+ SelectContent,
155
+ SelectLabel,
156
+ SelectItem,
157
+ SelectSeparator,
158
+ SelectScrollUpButton,
159
+ SelectScrollDownButton,
160
+ };
frontend/components/ui/typewriter.tsx ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { cn } from "@/app/utils";
4
+ import { motion } from "framer-motion";
5
+
6
+ export const TypewriterEffect = ({
7
+ words,
8
+ className,
9
+ }: {
10
+ words: string[];
11
+ className?: string;
12
+ cursorClassName?: string;
13
+ }) => {
14
+ const renderWords = () => {
15
+ return (
16
+ <div>
17
+ {words.map((word, idx) => {
18
+ return (
19
+ <div key={`word-${idx}`} className="inline-block">
20
+ {word.split("").map((char, index) => (
21
+ <span key={`char-${index}`}>{char}</span>
22
+ ))}
23
+ &nbsp;
24
+ </div>
25
+ );
26
+ })}
27
+ </div>
28
+ );
29
+ };
30
+
31
+ return (
32
+ <div className={cn("flex", className)}>
33
+ {words.length < 1 ? (
34
+ <span>...</span>
35
+ ) : (
36
+ <motion.div
37
+ className="overflow-hidden"
38
+ initial={{
39
+ width: "0%",
40
+ }}
41
+ whileInView={{
42
+ width: "fit-content",
43
+ }}
44
+ transition={{
45
+ duration: 0.5,
46
+ ease: "linear",
47
+ delay: 1,
48
+ }}
49
+ >
50
+ <div
51
+ style={{
52
+ whiteSpace: "nowrap",
53
+ }}
54
+ >
55
+ {renderWords()}
56
+ </div>
57
+ </motion.div>
58
+ )}
59
+ </div>
60
+ );
61
+ };
frontend/env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ SITE_URL=
frontend/next.config.mjs ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ output: "export",
4
+
5
+ async rewrites() {
6
+ return [
7
+ {
8
+ source: "/:path*",
9
+ destination: "http://localhost:7860/:path*",
10
+ },
11
+ ];
12
+ },
13
+ };
14
+
15
+ export default nextConfig;
frontend/package.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@daily-co/daily-js": "^0.62.0",
13
+ "@daily-co/daily-react": "^0.18.0",
14
+ "@radix-ui/react-select": "^2.0.0",
15
+ "@radix-ui/react-slot": "^1.0.2",
16
+ "@tabler/icons-react": "^3.1.0",
17
+ "class-variance-authority": "^0.7.0",
18
+ "clsx": "^2.1.0",
19
+ "framer-motion": "^11.0.27",
20
+ "next": "14.1.4",
21
+ "react": "^18",
22
+ "react-dom": "^18",
23
+ "recoil": "^0.7.7",
24
+ "tailwind-merge": "^2.2.2",
25
+ "tailwindcss-animate": "^1.0.7"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^20",
29
+ "@types/react": "^18",
30
+ "@types/react-dom": "^18",
31
+ "autoprefixer": "^10.0.1",
32
+ "eslint": "^8",
33
+ "eslint-config-next": "14.1.4",
34
+ "postcss": "^8",
35
+ "tailwindcss": "^3.4.3",
36
+ "typescript": "^5"
37
+ }
38
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
frontend/public/alpha-mask.gif ADDED
frontend/public/bg.jpg ADDED
frontend/tailwind.config.ts ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from "tailwindcss";
2
+
3
+ import { fontFamily } from "tailwindcss/defaultTheme";
4
+
5
+ const config = {
6
+ darkMode: ["class"],
7
+ content: [
8
+ "./pages/**/*.{ts,tsx}",
9
+ "./components/**/*.{ts,tsx}",
10
+ "./app/**/*.{ts,tsx}",
11
+ "./src/**/*.{ts,tsx}",
12
+ ],
13
+ prefix: "",
14
+ theme: {
15
+ container: {
16
+ center: true,
17
+ padding: "2rem",
18
+ screens: {
19
+ "2xl": "1400px",
20
+ },
21
+ },
22
+ extend: {
23
+ fontFamily: {
24
+ sans: ["var(--font-sans)", ...fontFamily.sans],
25
+ mono: ["var(--font-mono)", ...fontFamily.mono],
26
+ },
27
+ colors: {
28
+ border: "hsl(var(--border))",
29
+ input: "hsl(var(--input))",
30
+ ring: "hsl(var(--ring))",
31
+ background: "hsl(var(--background))",
32
+ foreground: "hsl(var(--foreground))",
33
+ primary: {
34
+ DEFAULT: "hsl(var(--primary))",
35
+ foreground: "hsl(var(--primary-foreground))",
36
+ },
37
+ secondary: {
38
+ DEFAULT: "hsl(var(--secondary))",
39
+ foreground: "hsl(var(--secondary-foreground))",
40
+ },
41
+ destructive: {
42
+ DEFAULT: "hsl(var(--destructive))",
43
+ foreground: "hsl(var(--destructive-foreground))",
44
+ },
45
+ muted: {
46
+ DEFAULT: "hsl(var(--muted))",
47
+ foreground: "hsl(var(--muted-foreground))",
48
+ },
49
+ accent: {
50
+ DEFAULT: "hsl(var(--accent))",
51
+ foreground: "hsl(var(--accent-foreground))",
52
+ },
53
+ popover: {
54
+ DEFAULT: "hsl(var(--popover))",
55
+ foreground: "hsl(var(--popover-foreground))",
56
+ },
57
+ card: {
58
+ DEFAULT: "hsl(var(--card))",
59
+ foreground: "hsl(var(--card-foreground))",
60
+ },
61
+ },
62
+ borderRadius: {
63
+ lg: "var(--radius)",
64
+ md: "calc(var(--radius) - 2px)",
65
+ sm: "calc(var(--radius) - 4px)",
66
+ },
67
+ keyframes: {
68
+ "accordion-down": {
69
+ from: { height: "0" },
70
+ to: { height: "var(--radix-accordion-content-height)" },
71
+ },
72
+ "accordion-up": {
73
+ from: { height: "var(--radix-accordion-content-height)" },
74
+ to: { height: "0" },
75
+ },
76
+ },
77
+ animation: {
78
+ "accordion-down": "accordion-down 0.2s ease-out",
79
+ "accordion-up": "accordion-up 0.2s ease-out",
80
+ },
81
+ },
82
+ },
83
+ plugins: [require("tailwindcss-animate")],
84
+ } satisfies Config;
85
+
86
+ export default config;
frontend/tsconfig.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "lib": [
4
+ "dom",
5
+ "dom.iterable",
6
+ "esnext"
7
+ ],
8
+ "allowJs": true,
9
+ "skipLibCheck": true,
10
+ "strict": true,
11
+ "noEmit": true,
12
+ "esModuleInterop": true,
13
+ "module": "esnext",
14
+ "moduleResolution": "bundler",
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "jsx": "preserve",
18
+ "incremental": true,
19
+ "plugins": [
20
+ {
21
+ "name": "next"
22
+ }
23
+ ],
24
+ "paths": {
25
+ "@/*": [
26
+ "./*"
27
+ ]
28
+ }
29
+ },
30
+ "include": [
31
+ "next-env.d.ts",
32
+ "**/*.ts",
33
+ "**/*.tsx",
34
+ ".next/types/**/*.ts",
35
+ "build/types/**/*.ts"
36
+ ],
37
+ "exclude": [
38
+ "node_modules"
39
+ ]
40
+ }
frontend/yarn.lock ADDED
The diff for this file is too large to render. See raw diff
 
image.png ADDED

Git LFS Details

  • SHA256: 5203e3d84d655048d01657bf8ac1679272d74f6c43c1879691ed0eb681d22c6f
  • Pointer size: 132 Bytes
  • Size of remote file: 1.26 MB
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ async_timeout
2
+ fastapi
3
+ uvicorn
4
+ requests
5
+ python-dotenv
6
+ pipecat-ai[daily,openai,fal]
src/assets/book1.png ADDED
src/assets/book2.png ADDED
src/assets/ding.wav ADDED
Binary file (16.7 kB). View file
 
src/assets/listening.wav ADDED
Binary file (26.1 kB). View file
 
src/assets/talking.wav ADDED
Binary file (24.4 kB). View file
 
src/bot.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import asyncio
3
+ import aiohttp
4
+ import os
5
+ import sys
6
+
7
+
8
+ from pipecat.frames.frames import LLMMessagesFrame, StopTaskFrame, EndFrame
9
+ from pipecat.pipeline.pipeline import Pipeline
10
+ from pipecat.pipeline.runner import PipelineRunner
11
+ from pipecat.pipeline.task import PipelineTask
12
+ from pipecat.processors.aggregators.llm_response import LLMAssistantResponseAggregator, LLMUserResponseAggregator
13
+ from pipecat.services.elevenlabs import ElevenLabsTTSService
14
+ from pipecat.services.fal import FalImageGenService
15
+ from pipecat.services.openai import OpenAILLMService,OpenAITTSService
16
+ from pipecat.transports.services.daily import DailyParams, DailyTransport, DailyTransportMessageFrame
17
+
18
+ from processors import StoryProcessor, StoryImageProcessor
19
+ from prompts import LLM_BASE_PROMPT, LLM_INTRO_PROMPT, CUE_USER_TURN
20
+ from utils.helpers import load_sounds, load_images
21
+
22
+ from loguru import logger
23
+
24
+ from dotenv import load_dotenv
25
+ load_dotenv(override=True)
26
+
27
+ logger.remove(0)
28
+ logger.add(sys.stderr, level="DEBUG")
29
+
30
+ sounds = load_sounds(["listening.wav"])
31
+ images = load_images(["book1.png", "book2.png"])
32
+
33
+
34
+ async def main(room_url, token=None):
35
+ async with aiohttp.ClientSession() as session:
36
+
37
+ # -------------- Transport --------------- #
38
+
39
+ transport = DailyTransport(
40
+ room_url,
41
+ token,
42
+ "Storytelling Bot",
43
+ DailyParams(
44
+ audio_out_enabled=True,
45
+ camera_out_enabled=True,
46
+ camera_out_width=768,
47
+ camera_out_height=768,
48
+ transcription_enabled=True,
49
+ vad_enabled=True,
50
+ )
51
+ )
52
+
53
+ logger.debug("Transport created for room:" + room_url)
54
+
55
+ # -------------- Services --------------- #
56
+
57
+ llm_service = OpenAILLMService(
58
+ api_key=os.getenv("OPENAI_API_KEY"),
59
+ model="gpt-3.5-turbo"
60
+ )
61
+
62
+ # tts_service = ElevenLabsTTSService(
63
+ # aiohttp_session=session,
64
+ # api_key=os.getenv("ELEVENLABS_API_KEY"),
65
+ # voice_id=os.getenv("ELEVENLABS_VOICE_ID"),
66
+ # )
67
+ tts_service=OpenAITTSService()
68
+
69
+ fal_service_params = FalImageGenService.InputParams(
70
+ image_size={
71
+ "width": 768,
72
+ "height": 768
73
+ }
74
+ )
75
+
76
+ fal_service = FalImageGenService(
77
+ aiohttp_session=session,
78
+ model="fal-ai/fast-lightning-sdxl",
79
+ params=fal_service_params,
80
+ key=os.getenv("FAL_KEY"),
81
+ )
82
+
83
+ # --------------- Setup ----------------- #
84
+
85
+ message_history = [LLM_BASE_PROMPT]
86
+ story_pages = []
87
+
88
+ # We need aggregators to keep track of user and LLM responses
89
+ llm_responses = LLMAssistantResponseAggregator(message_history)
90
+ user_responses = LLMUserResponseAggregator(message_history)
91
+
92
+ # -------------- Processors ------------- #
93
+
94
+ story_processor = StoryProcessor(message_history, story_pages)
95
+ image_processor = StoryImageProcessor(fal_service)
96
+
97
+ # -------------- Story Loop ------------- #
98
+
99
+ runner = PipelineRunner()
100
+
101
+ # The intro pipeline is used to start
102
+ # the story (as per LLM_INTRO_PROMPT)
103
+ intro_pipeline = Pipeline([llm_service, tts_service, transport.output()])
104
+
105
+ intro_task = PipelineTask(intro_pipeline)
106
+
107
+ logger.debug("Waiting for participant...")
108
+
109
+ @transport.event_handler("on_first_participant_joined")
110
+ async def on_first_participant_joined(transport, participant):
111
+ logger.debug("Participant joined, storytime commence!")
112
+ transport.capture_participant_transcription(participant["id"])
113
+ await intro_task.queue_frames(
114
+ [
115
+ images['book1'],
116
+ LLMMessagesFrame([LLM_INTRO_PROMPT]),
117
+ DailyTransportMessageFrame(CUE_USER_TURN),
118
+ sounds["listening"],
119
+ images['book2'],
120
+ StopTaskFrame()
121
+ ]
122
+ )
123
+
124
+ # We run the intro pipeline. This will start the transport. The intro
125
+ # task will exit after StopTaskFrame is processed.
126
+ await runner.run(intro_task)
127
+
128
+ # The main story pipeline is used to continue the story based on user
129
+ # input.
130
+ main_pipeline = Pipeline([
131
+ transport.input(),
132
+ user_responses,
133
+ llm_service,
134
+ story_processor,
135
+ image_processor,
136
+ tts_service,
137
+ transport.output(),
138
+ llm_responses
139
+ ])
140
+
141
+ main_task = PipelineTask(main_pipeline)
142
+
143
+ @transport.event_handler("on_participant_left")
144
+ async def on_participant_left(transport, participant, reason):
145
+ intro_task.queue_frame(EndFrame())
146
+ await main_task.queue_frame(EndFrame())
147
+
148
+ @transport.event_handler("on_call_state_updated")
149
+ async def on_call_state_updated(transport, state):
150
+ if state == "left":
151
+ await main_task.queue_frame(EndFrame())
152
+
153
+ await runner.run(main_task)
154
+
155
+ if __name__ == "__main__":
156
+ parser = argparse.ArgumentParser(description="Daily Storyteller Bot")
157
+ parser.add_argument("-u", type=str, help="Room URL")
158
+ parser.add_argument("-t", type=str, help="Token")
159
+ config = parser.parse_args()
160
+
161
+ asyncio.run(main(config.u, config.t))
src/bot_runner.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import argparse
3
+ import subprocess
4
+ import requests
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from fastapi import FastAPI, Request, HTTPException
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.staticfiles import StaticFiles
11
+ from fastapi.responses import FileResponse, JSONResponse
12
+
13
+ from pipecat.transports.services.helpers.daily_rest import DailyRESTHelper, DailyRoomObject, DailyRoomProperties, DailyRoomParams
14
+
15
+
16
+ from dotenv import load_dotenv
17
+ load_dotenv(override=True)
18
+
19
+ # ------------ Fast API Config ------------ #
20
+
21
+ MAX_SESSION_TIME = 8 * 60 # 5 minutes
22
+
23
+ daily_rest_helper = DailyRESTHelper(
24
+ os.getenv("DAILY_API_KEY", ""),
25
+ os.getenv("DAILY_API_URL", 'https://api.daily.co/v1'))
26
+
27
+
28
+ app = FastAPI()
29
+
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=["*"],
33
+ allow_credentials=True,
34
+ allow_methods=["*"],
35
+ allow_headers=["*"],
36
+ )
37
+
38
+ # Mount the static directory
39
+ STATIC_DIR = "frontend/out"
40
+
41
+
42
+ # ------------ Fast API Routes ------------ #
43
+
44
+ app.mount("/static", StaticFiles(directory=STATIC_DIR, html=True), name="static")
45
+
46
+
47
+ @app.post("/start_bot")
48
+ async def start_bot(request: Request) -> JSONResponse:
49
+ if os.getenv("ENV", "dev") == "production":
50
+ # Only allow requests from the specified domain
51
+ host_header = request.headers.get("host")
52
+ allowed_domains = ["storytelling-chatbot.fly.dev", "www.storytelling-chatbot.fly.dev"]
53
+ # Check if the Host header matches the allowed domain
54
+ if host_header not in allowed_domains:
55
+ raise HTTPException(status_code=403, detail="Access denied")
56
+
57
+ try:
58
+ data = await request.json()
59
+ # Is this a webhook creation request?
60
+ if "test" in data:
61
+ return JSONResponse({"test": True})
62
+ except Exception as e:
63
+ pass
64
+
65
+ # Use specified room URL, or create a new one if not specified
66
+ room_url = os.getenv("DAILY_SAMPLE_ROOM_URL", "")
67
+
68
+ if not room_url:
69
+ params = DailyRoomParams(
70
+ properties=DailyRoomProperties()
71
+ )
72
+ try:
73
+ room: DailyRoomObject = daily_rest_helper.create_room(params=params)
74
+ except Exception as e:
75
+ raise HTTPException(
76
+ status_code=500,
77
+ detail=f"Unable to provision room {e}")
78
+ else:
79
+ # Check passed room URL exists, we should assume that it already has a sip set up
80
+ try:
81
+ room: DailyRoomObject = daily_rest_helper.get_room_from_url(room_url)
82
+ except Exception:
83
+ raise HTTPException(
84
+ status_code=500, detail=f"Room not found: {room_url}")
85
+
86
+ # Give the agent a token to join the session
87
+ token = daily_rest_helper.get_token(room.url, MAX_SESSION_TIME)
88
+
89
+ if not room or not token:
90
+ raise HTTPException(
91
+ status_code=500, detail=f"Failed to get token for room: {room_url}")
92
+
93
+ # Launch a new VM, or run as a shell process (not recommended)
94
+ if os.getenv("RUN_AS_VM", False):
95
+ try:
96
+ virtualize_bot(room.url, token)
97
+ except Exception as e:
98
+ raise HTTPException(
99
+ status_code=500, detail=f"Failed to spawn VM: {e}")
100
+ else:
101
+ try:
102
+ subprocess.Popen(
103
+ [f"python3 -m bot -u {room.url} -t {token}"],
104
+ shell=True,
105
+ bufsize=1,
106
+ cwd=os.path.dirname(os.path.abspath(__file__)))
107
+ except Exception as e:
108
+ raise HTTPException(
109
+ status_code=500, detail=f"Failed to start subprocess: {e}")
110
+
111
+ # Grab a token for the user to join with
112
+ user_token = daily_rest_helper.get_token(room.url, MAX_SESSION_TIME)
113
+
114
+ return JSONResponse({
115
+ "room_url": room.url,
116
+ "token": user_token,
117
+ })
118
+
119
+
120
+ @app.get("/{path_name:path}", response_class=FileResponse)
121
+ async def catch_all(path_name: Optional[str] = ""):
122
+ if path_name == "":
123
+ return FileResponse(f"{STATIC_DIR}/index.html")
124
+
125
+ file_path = Path(STATIC_DIR) / (path_name or "")
126
+
127
+ if file_path.is_file():
128
+ return file_path
129
+
130
+ html_file_path = file_path.with_suffix(".html")
131
+ if html_file_path.is_file():
132
+ return FileResponse(html_file_path)
133
+
134
+ raise HTTPException(status_code=450, detail="Incorrect API call")
135
+
136
+
137
+ # ------------ Virtualization ------------ #
138
+
139
+ def virtualize_bot(room_url: str, token: str):
140
+ """
141
+ This is an example of how to virtualize the bot using Fly.io
142
+ You can adapt this method to use whichever cloud provider you prefer.
143
+ """
144
+ FLY_API_HOST = os.getenv("FLY_API_HOST", "https://api.machines.dev/v1")
145
+ FLY_APP_NAME = os.getenv("FLY_APP_NAME", "storytelling-chatbot")
146
+ FLY_API_KEY = os.getenv("FLY_API_KEY", "")
147
+ FLY_HEADERS = {
148
+ 'Authorization': f"Bearer {FLY_API_KEY}",
149
+ 'Content-Type': 'application/json'
150
+ }
151
+
152
+ # Use the same image as the bot runner
153
+ res = requests.get(f"{FLY_API_HOST}/apps/{FLY_APP_NAME}/machines", headers=FLY_HEADERS)
154
+ if res.status_code != 200:
155
+ raise Exception(f"Unable to get machine info from Fly: {res.text}")
156
+ image = res.json()[0]['config']['image']
157
+
158
+ # Machine configuration
159
+ cmd = f"python3 src/bot.py -u {room_url} -t {token}"
160
+ cmd = cmd.split()
161
+ worker_props = {
162
+ "config": {
163
+ "image": image,
164
+ "auto_destroy": True,
165
+ "init": {
166
+ "cmd": cmd
167
+ },
168
+ "restart": {
169
+ "policy": "no"
170
+ },
171
+ "guest": {
172
+ "cpu_kind": "shared",
173
+ "cpus": 1,
174
+ "memory_mb": 512
175
+ }
176
+ },
177
+
178
+ }
179
+
180
+ # Spawn a new machine instance
181
+ res = requests.post(
182
+ f"{FLY_API_HOST}/apps/{FLY_APP_NAME}/machines",
183
+ headers=FLY_HEADERS,
184
+ json=worker_props)
185
+
186
+ if res.status_code != 200:
187
+ raise Exception(f"Problem starting a bot worker: {res.text}")
188
+
189
+ # Wait for the machine to enter the started state
190
+ vm_id = res.json()['id']
191
+
192
+ res = requests.get(
193
+ f"{FLY_API_HOST}/apps/{FLY_APP_NAME}/machines/{vm_id}/wait?state=started",
194
+ headers=FLY_HEADERS)
195
+
196
+ if res.status_code != 200:
197
+ raise Exception(f"Bot was unable to enter started state: {res.text}")
198
+
199
+ print(f"Machine joined room: {room_url}")
200
+
201
+
202
+ # ------------ Main ------------ #
203
+
204
+ if __name__ == "__main__":
205
+ # Check environment variables
206
+ required_env_vars = ['OPENAI_API_KEY', 'DAILY_API_KEY',
207
+ 'FAL_KEY', 'OPENAI_BASE_URL']
208
+ for env_var in required_env_vars:
209
+ if env_var not in os.environ:
210
+ raise Exception(f"Missing environment variable: {env_var}.")
211
+
212
+ import uvicorn
213
+
214
+ default_host = os.getenv("HOST", "0.0.0.0")
215
+ default_port = int(os.getenv("FAST_API_PORT", "7860"))
216
+
217
+ parser = argparse.ArgumentParser(
218
+ description="Daily Storyteller FastAPI server")
219
+ parser.add_argument("--host", type=str,
220
+ default=default_host, help="Host address")
221
+ parser.add_argument("--port", type=int,
222
+ default=default_port, help="Port number")
223
+ parser.add_argument("--reload", action="store_true",
224
+ help="Reload code on change")
225
+
226
+ config = parser.parse_args()
227
+
228
+ uvicorn.run(
229
+ "bot_runner:app",
230
+ host=config.host,
231
+ port=config.port,
232
+ reload=config.reload
233
+ )