Spaces:
Sleeping
Sleeping
Commit
·
51669fc
0
Parent(s):
original code by JeffLiang and Radames
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .DS_Store +0 -0
- Dockerfile +48 -0
- LICENSE +53 -0
- README.md +17 -0
- config.py +109 -0
- connection_manager.py +116 -0
- frontend/.eslintignore +13 -0
- frontend/.eslintrc.cjs +30 -0
- frontend/.gitignore +10 -0
- frontend/.npmrc +1 -0
- frontend/.prettierignore +13 -0
- frontend/.prettierrc +19 -0
- frontend/README.md +38 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +41 -0
- frontend/postcss.config.js +6 -0
- frontend/src/app.css +3 -0
- frontend/src/app.d.ts +12 -0
- frontend/src/app.html +12 -0
- frontend/src/lib/components/Button.svelte +15 -0
- frontend/src/lib/components/Checkbox.svelte +14 -0
- frontend/src/lib/components/ImagePlayer.svelte +50 -0
- frontend/src/lib/components/InputRange.svelte +50 -0
- frontend/src/lib/components/MediaListSwitcher.svelte +40 -0
- frontend/src/lib/components/PipelineOptions.svelte +61 -0
- frontend/src/lib/components/SeedInput.svelte +26 -0
- frontend/src/lib/components/Selectlist.svelte +24 -0
- frontend/src/lib/components/TextArea.svelte +23 -0
- frontend/src/lib/components/VideoInput.svelte +109 -0
- frontend/src/lib/components/Warning.svelte +27 -0
- frontend/src/lib/icons/floppy.svelte +10 -0
- frontend/src/lib/icons/screen.svelte +10 -0
- frontend/src/lib/icons/spinner.svelte +10 -0
- frontend/src/lib/index.ts +1 -0
- frontend/src/lib/lcmLive.ts +99 -0
- frontend/src/lib/mediaStream.ts +98 -0
- frontend/src/lib/store.ts +15 -0
- frontend/src/lib/types.ts +40 -0
- frontend/src/lib/utils.ts +38 -0
- frontend/src/routes/+layout.svelte +5 -0
- frontend/src/routes/+page.svelte +152 -0
- frontend/src/routes/+page.ts +1 -0
- frontend/static/favicon.png +0 -0
- frontend/svelte.config.js +17 -0
- frontend/tailwind.config.js +8 -0
- frontend/tsconfig.json +17 -0
- frontend/vite.config.ts +15 -0
- main.py +193 -0
- requirements.txt +14 -0
- start.sh +11 -0
.DS_Store
ADDED
Binary file (6.15 kB). View file
|
|
Dockerfile
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM nvidia/cuda:12.1.1-cudnn8-devel-ubuntu22.04
|
2 |
+
|
3 |
+
ARG DEBIAN_FRONTEND=noninteractive
|
4 |
+
|
5 |
+
ENV PYTHONUNBUFFERED=1
|
6 |
+
ENV NODE_MAJOR=20
|
7 |
+
|
8 |
+
RUN apt-get update && apt-get install --no-install-recommends -y \
|
9 |
+
build-essential \
|
10 |
+
python3.9 \
|
11 |
+
python3-pip \
|
12 |
+
python3-dev \
|
13 |
+
git \
|
14 |
+
ffmpeg \
|
15 |
+
google-perftools \
|
16 |
+
ca-certificates curl gnupg \
|
17 |
+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
18 |
+
|
19 |
+
WORKDIR /code
|
20 |
+
|
21 |
+
RUN mkdir -p /etc/apt/keyrings
|
22 |
+
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
23 |
+
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
|
24 |
+
RUN apt-get update && apt-get install nodejs -y
|
25 |
+
|
26 |
+
COPY ./requirements.txt /code/requirements.txt
|
27 |
+
|
28 |
+
# Set up a new user named "user" with user ID 1000
|
29 |
+
RUN useradd -m -u 1000 user
|
30 |
+
# Switch to the "user" user
|
31 |
+
USER user
|
32 |
+
# Set home to the user's home directory
|
33 |
+
ENV HOME=/home/user \
|
34 |
+
PATH=/home/user/.local/bin:$PATH \
|
35 |
+
PYTHONPATH=$HOME/app \
|
36 |
+
PYTHONUNBUFFERED=1 \
|
37 |
+
SYSTEM=spaces
|
38 |
+
|
39 |
+
RUN pip3 install --no-cache-dir --upgrade --pre -r /code/requirements.txt
|
40 |
+
|
41 |
+
# Set the working directory to the user's home directory
|
42 |
+
WORKDIR $HOME/app
|
43 |
+
|
44 |
+
# Copy the current directory contents into the container at $HOME/app setting the owner to the user
|
45 |
+
COPY --chown=user . $HOME/app
|
46 |
+
|
47 |
+
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libtcmalloc.so.4
|
48 |
+
CMD ["./build-run.sh"]
|
LICENSE
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Stream V2V
|
2 |
+
8417 MAR
|
3 |
+
UT AUSTIN RESEARCH LICENSE
|
4 |
+
(NONCONFIDENTIAL SOURCE CODE)
|
5 |
+
|
6 |
+
The University of Texas at Austin has developed certain software and documentation that it desires to make available without charge to anyone for academic, research, experimental or personal use. If you wish to distribute or make other use of the software, you may purchase a license to do so from The University of Texas at Austin ([email protected]).
|
7 |
+
The accompanying source code is made available to you under the terms of this UT Research License (this “UTRL”). By installing or using the code, you are consenting to be bound by this UTRL. If you do not agree to the terms and conditions of this license, do not install or use any part of the code.
|
8 |
+
The terms and conditions in this UTRL not only apply to the source code made available by Licensor, but also to any improvements to, or derivative works of, that source code made by you and to any object code compiled from such source code, improvements or derivative works.
|
9 |
+
|
10 |
+
1. DEFINITIONS.
|
11 |
+
1.1 “Commercial Use” shall mean use of Software or Documentation by Licensee for direct or indirect financial, commercial or strategic gain or advantage, including without limitation: (a) bundling or integrating the Software with any hardware product or another software product for transfer, sale or license to a third party (even if distributing the Software on separate media and not charging for the Software); (b) providing customers with a link to the Software or a copy of the Software for use with hardware or another software product purchased by that customer; or (c) use in connection with the performance of services for which Licensee is compensated.
|
12 |
+
1.2 “Derivative Products” means any improvements to, or other derivative works of, the Software made by Licensee, and any computer software programs, and accompanying documentation, developed by Licensee which are a modification of, enhancement to, derived from or based upon the Licensed Software or documentation provided by Licensor for the Licensed Software, and any object code compiled from such computer software programs.
|
13 |
+
1.3 “Documentation” shall mean all manuals, user documentation, and other related materials pertaining to the Software that are made available to Licensee in connection with the Software.
|
14 |
+
1.4 “Licensor” shall mean The University of Texas at Austin, on behalf of the Board of Regents of the University of Texas System, an agency of the State of Texas, whose address is 3925 W. Braker Lane, Suite 1.9A (R3500), Austin, Texas 78759.
|
15 |
+
1.5 “Licensee” or “you” shall mean the person or entity that has agreed to the terms hereof and is exercising rights granted hereunder.
|
16 |
+
1.6 “Software” shall mean the computer program(s) referred to as: “Stream V2V” (UT Tech ID 8417 MAR), which is made available under this UTRL in source code form, including any error corrections, bug fixes, patches, updates or other modifications that Licensor may in its sole discretion make available to Licensee from time to time, and any object code compiled from such source code.
|
17 |
+
|
18 |
+
2. GRANT OF RIGHTS.
|
19 |
+
Subject to the terms and conditions hereunder, Licensor hereby grants to Licensee a worldwide, non-transferable, non-exclusive license to (a) install, use and reproduce the Software for academic, research, experimental and personal use (but specifically excluding Commercial Use); (b) use and modify the Software to create Derivative Products, subject to Section 3.2; (c) use the Documentation, if any, solely in connection with Licensee’s authorized use of the Software; and (d) a non-exclusive, royalty-free license for academic, research, experimental and personal use (but specifically excluding Commercial Use) to those patents, of which Diana Marculescu or Jeff Liang is a named inventor, that are licensable by Licensee and that are necessarily infringed by such authorized use of the Software, and solely in connection with Licensee’s authorized use of the Software.
|
20 |
+
|
21 |
+
3. RESTRICTIONS; COVENANTS.
|
22 |
+
3.1 Licensee may not: (a) distribute, sub-license or otherwise transfer copies or rights to the Software (or any portion thereof) or the Documentation; (b) use the Software (or any portion thereof) or Documentation for Commercial Use, or for any other use except as described in Section 2; (c) copy the Software or Documentation other than for archival and backup purposes; or (d) remove any product identification, copyright, proprietary notices or labels from the Software and Documentation. This UTRL confers no rights upon Licensee except those expressly granted herein.
|
23 |
+
3.2 Derivative Products. Licensee hereby agrees that it will provide a copy of all Derivative Products to Licensor and that its use of the Derivative Products will be subject to all of the same terms, conditions, restrictions and limitations on use imposed on the Software under this UTRL. Licensee hereby grants Licensor a worldwide, non-exclusive, royalty-free license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense and distribute Derivative Products. Licensee also hereby grants Licensor a worldwide, non-exclusive, royalty-free patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Derivative Products under those patent claims, from patents of which Diana Marculescu or Jeff Liang is a named inventor, that licensable by Licensee that are necessarily infringed by the Derivative Products.
|
24 |
+
|
25 |
+
4. CONFIDENTIALITY; PROTECTION OF SOFTWARE.
|
26 |
+
4.1 Reserved.
|
27 |
+
4.2 Proprietary Notices. Licensee shall maintain and place on any copy of Software or Documentation that it reproduces for internal use all notices as are authorized and/or required hereunder. Licensee shall include a copy of this UTRL and the following notice, on each copy of the Software and Documentation. Such license and notice shall be embedded in each copy of the Software, in the video screen display, on the physical medium embodying the Software copy and on any Documentation:
|
28 |
+
Copyright © 2021, The University of Texas at Austin. All rights reserved.
|
29 |
+
UNIVERSITY EXPRESSLY DISCLAIMS ANY AND ALL WARRANTIES CONCERNING THIS SOFTWARE AND DOCUMENTATION, INCLUDING ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR ANY PARTICULAR PURPOSE, NON-INFRINGEMENT AND WARRANTIES OF PERFORMANCE, AND ANY WARRANTY THAT MIGHT OTHERWISE ARISE FROM COURSE OF DEALING OR USAGE OF TRADE. NO WARRANTY IS EITHER EXPRESS OR IMPLIED WITH RESPECT TO THE USE OF THE SOFTWARE OR DOCUMENTATION. Under no circumstances shall University be liable for incidental, special, indirect, direct or consequential damages or loss of profits, interruption of business, or related expenses which may arise from use of Software or Documentation, including but not limited to those resulting from defects in Software and/or Documentation, or loss or inaccuracy of data of any kind.
|
30 |
+
|
31 |
+
5. WARRANTIES.
|
32 |
+
5.1 Disclaimer of Warranties. TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE SOFTWARE AND DOCUMENTATION ARE BEING PROVIDED ON AN “AS IS” BASIS WITHOUT ANY WARRANTIES OF ANY KIND RESPECTING THE SOFTWARE OR DOCUMENTATION, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY OF DESIGN, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT.
|
33 |
+
5.2 Limitation of Liability. UNDER NO CIRCUMSTANCES UNLESS REQUIRED BY APPLICABLE LAW SHALL LICENSOR BE LIABLE FOR INCIDENTAL, SPECIAL, INDIRECT, DIRECT OR CONSEQUENTIAL DAMAGES OR LOSS OF PROFITS, INTERRUPTION OF BUSINESS, OR RELATED EXPENSES WHICH MAY ARISE AS A RESULT OF THIS LICENSE OR OUT OF THE USE OR ATTEMPT OF USE OF SOFTWARE OR DOCUMENTATION INCLUDING BUT NOT LIMITED TO THOSE RESULTING FROM DEFECTS IN SOFTWARE AND/OR DOCUMENTATION, OR LOSS OR INACCURACY OF DATA OF ANY KIND. THE FOREGOING EXCLUSIONS AND LIMITATIONS WILL APPLY TO ALL CLAIMS AND ACTIONS OF ANY KIND, WHETHER BASED ON CONTRACT, TORT (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE), OR ANY OTHER GROUNDS.
|
34 |
+
|
35 |
+
6. INDEMNIFICATION.
|
36 |
+
Licensee shall indemnify, defend and hold harmless Licensor, the University of Texas System, their Regents, and their officers, agents and employees from and against any claims, demands, or causes of action whatsoever caused by, or arising out of, or resulting from, the exercise or practice of the license granted hereunder by Licensee, its officers, employees, agents or representatives.
|
37 |
+
|
38 |
+
7. TERMINATION.
|
39 |
+
If Licensee breaches this UTRL, Licensee’s right to use the Software and Documentation will terminate immediately without notice, but all provisions of this UTRL except Section 2 will survive termination and continue in effect. Upon termination, Licensee must destroy all copies of the Software and Documentation.
|
40 |
+
8. GOVERNING LAW; JURISDICTION AND VENUE.
|
41 |
+
|
42 |
+
The validity, interpretation, construction and performance of this UTRL shall be governed by the laws of the State of Texas. The Texas state courts of Travis County, Texas (or, if there is exclusive federal jurisdiction, the United States District Court for the Western District of Texas) shall have exclusive jurisdiction and venue over any dispute arising out of this UTRL, and Licensee consents to the jurisdiction of such courts. Application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded.
|
43 |
+
|
44 |
+
9. EXPORT CONTROLS.
|
45 |
+
This license is subject to all applicable export restrictions. Licensee must comply with all export and import laws and restrictions and regulations of any United States or foreign agency or authority relating to the Software and its use.
|
46 |
+
|
47 |
+
10. U.S. GOVERNMENT END-USERS.
|
48 |
+
The Software is a “commercial item,” as that term is defined in 48 C.F.R. 2.101, consisting of “commercial computer software” and “commercial computer software documentation,” as such terms are used in 48 C.F.R. 12.212 (Sept. 1995) and 48 C.F.R. 227.7202 (June 1995). Consistent with 48 C.F.R. 12.212, 48 C.F.R. 27.405(b)(2) (June 1998) and 48 C.F.R. 227.7202, all U.S. Government End Users acquire the Software with only those rights as set forth herein.
|
49 |
+
|
50 |
+
11. MISCELLANEOUS
|
51 |
+
If any provision hereof shall be held illegal, invalid or unenforceable, in whole or in part, such provision shall be modified to the minimum extent necessary to make it legal, valid and enforceable, and the legality, validity and enforceability of all other provisions of this UTRL shall not be affected thereby. Licensee may not assign this UTRL in whole or in part, without Licensor’s prior written consent. Any attempt to assign this UTRL without such consent will be null and void. This UTRL is the complete and exclusive statement between Licensee and Licensor relating to the subject matter hereof and supersedes all prior oral and written and all contemporaneous oral negotiations, commitments and understandings of the parties, if any. Any waiver by either party of any default or breach hereunder shall not constitute a waiver of any provision of this UTRL or of any subsequent default or breach of the same or a different kind.
|
52 |
+
|
53 |
+
END OF LICENSE
|
README.md
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: Stream V2V Demo
|
3 |
+
emoji: 🖼️🖼️
|
4 |
+
colorFrom: gray
|
5 |
+
colorTo: indigo
|
6 |
+
sdk: docker
|
7 |
+
pinned: false
|
8 |
+
suggested_hardware: a10g-small
|
9 |
+
disable_embedding: true
|
10 |
+
---
|
11 |
+
|
12 |
+
Note: I am not the original author
|
13 |
+
|
14 |
+
This is a deployment of this demo by Jeff Liang: https://github.com/Jeff-LiangF/streamv2v/tree/main/demo_w_camera
|
15 |
+
|
16 |
+
based on code from Radamés Ajna (https://github.com/radames/Real-Time-Latent-Consistency-Model/tree/main)
|
17 |
+
|
config.py
ADDED
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import NamedTuple
|
2 |
+
import argparse
|
3 |
+
import os
|
4 |
+
|
5 |
+
|
6 |
+
class Args(NamedTuple):
|
7 |
+
host: str
|
8 |
+
port: int
|
9 |
+
reload: bool
|
10 |
+
mode: str
|
11 |
+
max_queue_size: int
|
12 |
+
timeout: float
|
13 |
+
safety_checker: bool
|
14 |
+
taesd: bool
|
15 |
+
ssl_certfile: str
|
16 |
+
ssl_keyfile: str
|
17 |
+
debug: bool
|
18 |
+
acceleration: str
|
19 |
+
engine_dir: str
|
20 |
+
|
21 |
+
def pretty_print(self):
|
22 |
+
print("\n")
|
23 |
+
for field, value in self._asdict().items():
|
24 |
+
print(f"{field}: {value}")
|
25 |
+
print("\n")
|
26 |
+
|
27 |
+
|
28 |
+
MAX_QUEUE_SIZE = int(os.environ.get("MAX_QUEUE_SIZE", 0))
|
29 |
+
TIMEOUT = float(os.environ.get("TIMEOUT", 0))
|
30 |
+
SAFETY_CHECKER = os.environ.get("SAFETY_CHECKER", None) == "True"
|
31 |
+
USE_TAESD = os.environ.get("USE_TAESD", "True") == "True"
|
32 |
+
ENGINE_DIR = os.environ.get("ENGINE_DIR", "engines")
|
33 |
+
ACCELERATION = os.environ.get("ACCELERATION", "xformers")
|
34 |
+
|
35 |
+
default_host = os.getenv("HOST", "0.0.0.0")
|
36 |
+
default_port = int(os.getenv("PORT", "7860"))
|
37 |
+
default_mode = os.getenv("MODE", "default")
|
38 |
+
|
39 |
+
parser = argparse.ArgumentParser(description="Run the app")
|
40 |
+
parser.add_argument("--host", type=str, default=default_host, help="Host address")
|
41 |
+
parser.add_argument("--port", type=int, default=default_port, help="Port number")
|
42 |
+
parser.add_argument("--reload", action="store_true", help="Reload code on change")
|
43 |
+
parser.add_argument(
|
44 |
+
"--mode", type=str, default=default_mode, help="App Inferece Mode: txt2img, img2img"
|
45 |
+
)
|
46 |
+
parser.add_argument(
|
47 |
+
"--max-queue-size",
|
48 |
+
dest="max_queue_size",
|
49 |
+
type=int,
|
50 |
+
default=MAX_QUEUE_SIZE,
|
51 |
+
help="Max Queue Size",
|
52 |
+
)
|
53 |
+
parser.add_argument("--timeout", type=float, default=TIMEOUT, help="Timeout")
|
54 |
+
parser.add_argument(
|
55 |
+
"--safety-checker",
|
56 |
+
dest="safety_checker",
|
57 |
+
action="store_true",
|
58 |
+
default=SAFETY_CHECKER,
|
59 |
+
help="Safety Checker",
|
60 |
+
)
|
61 |
+
parser.add_argument(
|
62 |
+
"--taesd",
|
63 |
+
dest="taesd",
|
64 |
+
action="store_true",
|
65 |
+
help="Use Tiny Autoencoder",
|
66 |
+
)
|
67 |
+
parser.add_argument(
|
68 |
+
"--no-taesd",
|
69 |
+
dest="taesd",
|
70 |
+
action="store_false",
|
71 |
+
help="Use Tiny Autoencoder",
|
72 |
+
)
|
73 |
+
parser.add_argument(
|
74 |
+
"--ssl-certfile",
|
75 |
+
dest="ssl_certfile",
|
76 |
+
type=str,
|
77 |
+
default=None,
|
78 |
+
help="SSL certfile",
|
79 |
+
)
|
80 |
+
parser.add_argument(
|
81 |
+
"--ssl-keyfile",
|
82 |
+
dest="ssl_keyfile",
|
83 |
+
type=str,
|
84 |
+
default=None,
|
85 |
+
help="SSL keyfile",
|
86 |
+
)
|
87 |
+
parser.add_argument(
|
88 |
+
"--debug",
|
89 |
+
action="store_true",
|
90 |
+
default=False,
|
91 |
+
help="Debug",
|
92 |
+
)
|
93 |
+
parser.add_argument(
|
94 |
+
"--acceleration",
|
95 |
+
type=str,
|
96 |
+
default=ACCELERATION,
|
97 |
+
choices=["none", "xformers", "sfast", "tensorrt"],
|
98 |
+
help="Acceleration",
|
99 |
+
)
|
100 |
+
parser.add_argument(
|
101 |
+
"--engine-dir",
|
102 |
+
dest="engine_dir",
|
103 |
+
type=str,
|
104 |
+
default=ENGINE_DIR,
|
105 |
+
help="Engine Dir",
|
106 |
+
)
|
107 |
+
parser.set_defaults(taesd=USE_TAESD)
|
108 |
+
config = Args(**vars(parser.parse_args()))
|
109 |
+
config.pretty_print()
|
connection_manager.py
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Dict, Union
|
2 |
+
from uuid import UUID
|
3 |
+
import asyncio
|
4 |
+
from fastapi import WebSocket
|
5 |
+
from starlette.websockets import WebSocketState
|
6 |
+
import logging
|
7 |
+
from types import SimpleNamespace
|
8 |
+
|
9 |
+
Connections = Dict[UUID, Dict[str, Union[WebSocket, asyncio.Queue]]]
|
10 |
+
|
11 |
+
|
12 |
+
class ServerFullException(Exception):
|
13 |
+
"""Exception raised when the server is full."""
|
14 |
+
|
15 |
+
pass
|
16 |
+
|
17 |
+
|
18 |
+
class ConnectionManager:
|
19 |
+
def __init__(self):
|
20 |
+
self.active_connections: Connections = {}
|
21 |
+
|
22 |
+
async def connect(
|
23 |
+
self, user_id: UUID, websocket: WebSocket, max_queue_size: int = 0
|
24 |
+
):
|
25 |
+
await websocket.accept()
|
26 |
+
user_count = self.get_user_count()
|
27 |
+
print(f"User count: {user_count}")
|
28 |
+
if max_queue_size > 0 and user_count >= max_queue_size:
|
29 |
+
print("Server is full")
|
30 |
+
await websocket.send_json({"status": "error", "message": "Server is full"})
|
31 |
+
await websocket.close()
|
32 |
+
raise ServerFullException("Server is full")
|
33 |
+
print(f"New user connected: {user_id}")
|
34 |
+
self.active_connections[user_id] = {
|
35 |
+
"websocket": websocket,
|
36 |
+
"queue": asyncio.Queue(),
|
37 |
+
}
|
38 |
+
await websocket.send_json(
|
39 |
+
{"status": "connected", "message": "Connected"},
|
40 |
+
)
|
41 |
+
await websocket.send_json({"status": "wait"})
|
42 |
+
await websocket.send_json({"status": "send_frame"})
|
43 |
+
|
44 |
+
def check_user(self, user_id: UUID) -> bool:
|
45 |
+
return user_id in self.active_connections
|
46 |
+
|
47 |
+
async def update_data(self, user_id: UUID, new_data: SimpleNamespace):
|
48 |
+
user_session = self.active_connections.get(user_id)
|
49 |
+
if user_session:
|
50 |
+
queue = user_session["queue"]
|
51 |
+
while not queue.empty():
|
52 |
+
try:
|
53 |
+
queue.get_nowait()
|
54 |
+
except asyncio.QueueEmpty:
|
55 |
+
continue
|
56 |
+
await queue.put(new_data)
|
57 |
+
|
58 |
+
async def get_latest_data(self, user_id: UUID) -> SimpleNamespace:
|
59 |
+
user_session = self.active_connections.get(user_id)
|
60 |
+
if user_session:
|
61 |
+
queue = user_session["queue"]
|
62 |
+
try:
|
63 |
+
return await queue.get()
|
64 |
+
except asyncio.QueueEmpty:
|
65 |
+
return None
|
66 |
+
|
67 |
+
def delete_user(self, user_id: UUID):
|
68 |
+
user_session = self.active_connections.pop(user_id, None)
|
69 |
+
if user_session:
|
70 |
+
queue = user_session["queue"]
|
71 |
+
while not queue.empty():
|
72 |
+
try:
|
73 |
+
queue.get_nowait()
|
74 |
+
except asyncio.QueueEmpty:
|
75 |
+
continue
|
76 |
+
|
77 |
+
def get_user_count(self) -> int:
|
78 |
+
return len(self.active_connections)
|
79 |
+
|
80 |
+
def get_websocket(self, user_id: UUID) -> WebSocket:
|
81 |
+
user_session = self.active_connections.get(user_id)
|
82 |
+
if user_session:
|
83 |
+
websocket = user_session["websocket"]
|
84 |
+
if websocket.client_state == WebSocketState.CONNECTED:
|
85 |
+
return user_session["websocket"]
|
86 |
+
return None
|
87 |
+
|
88 |
+
async def disconnect(self, user_id: UUID):
|
89 |
+
websocket = self.get_websocket(user_id)
|
90 |
+
if websocket:
|
91 |
+
await websocket.close()
|
92 |
+
self.delete_user(user_id)
|
93 |
+
|
94 |
+
async def send_json(self, user_id: UUID, data: Dict):
|
95 |
+
try:
|
96 |
+
websocket = self.get_websocket(user_id)
|
97 |
+
if websocket:
|
98 |
+
await websocket.send_json(data)
|
99 |
+
except Exception as e:
|
100 |
+
logging.error(f"Error: Send json: {e}")
|
101 |
+
|
102 |
+
async def receive_json(self, user_id: UUID) -> Dict:
|
103 |
+
try:
|
104 |
+
websocket = self.get_websocket(user_id)
|
105 |
+
if websocket:
|
106 |
+
return await websocket.receive_json()
|
107 |
+
except Exception as e:
|
108 |
+
logging.error(f"Error: Receive json: {e}")
|
109 |
+
|
110 |
+
async def receive_bytes(self, user_id: UUID) -> bytes:
|
111 |
+
try:
|
112 |
+
websocket = self.get_websocket(user_id)
|
113 |
+
if websocket:
|
114 |
+
return await websocket.receive_bytes()
|
115 |
+
except Exception as e:
|
116 |
+
logging.error(f"Error: Receive bytes: {e}")
|
frontend/.eslintignore
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.DS_Store
|
2 |
+
node_modules
|
3 |
+
/build
|
4 |
+
/.svelte-kit
|
5 |
+
/package
|
6 |
+
.env
|
7 |
+
.env.*
|
8 |
+
!.env.example
|
9 |
+
|
10 |
+
# Ignore files for PNPM, NPM and YARN
|
11 |
+
pnpm-lock.yaml
|
12 |
+
package-lock.json
|
13 |
+
yarn.lock
|
frontend/.eslintrc.cjs
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module.exports = {
|
2 |
+
root: true,
|
3 |
+
extends: [
|
4 |
+
'eslint:recommended',
|
5 |
+
'plugin:@typescript-eslint/recommended',
|
6 |
+
'plugin:svelte/recommended',
|
7 |
+
'prettier'
|
8 |
+
],
|
9 |
+
parser: '@typescript-eslint/parser',
|
10 |
+
plugins: ['@typescript-eslint'],
|
11 |
+
parserOptions: {
|
12 |
+
sourceType: 'module',
|
13 |
+
ecmaVersion: 2020,
|
14 |
+
extraFileExtensions: ['.svelte']
|
15 |
+
},
|
16 |
+
env: {
|
17 |
+
browser: true,
|
18 |
+
es2017: true,
|
19 |
+
node: true
|
20 |
+
},
|
21 |
+
overrides: [
|
22 |
+
{
|
23 |
+
files: ['*.svelte'],
|
24 |
+
parser: 'svelte-eslint-parser',
|
25 |
+
parserOptions: {
|
26 |
+
parser: '@typescript-eslint/parser'
|
27 |
+
}
|
28 |
+
}
|
29 |
+
]
|
30 |
+
};
|
frontend/.gitignore
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.DS_Store
|
2 |
+
node_modules
|
3 |
+
/build
|
4 |
+
/.svelte-kit
|
5 |
+
/package
|
6 |
+
.env
|
7 |
+
.env.*
|
8 |
+
!.env.example
|
9 |
+
vite.config.js.timestamp-*
|
10 |
+
vite.config.ts.timestamp-*
|
frontend/.npmrc
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
engine-strict=true
|
frontend/.prettierignore
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.DS_Store
|
2 |
+
node_modules
|
3 |
+
/build
|
4 |
+
/.svelte-kit
|
5 |
+
/package
|
6 |
+
.env
|
7 |
+
.env.*
|
8 |
+
!.env.example
|
9 |
+
|
10 |
+
# Ignore files for PNPM, NPM and YARN
|
11 |
+
pnpm-lock.yaml
|
12 |
+
package-lock.json
|
13 |
+
yarn.lock
|
frontend/.prettierrc
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"useTabs": false,
|
3 |
+
"singleQuote": true,
|
4 |
+
"trailingComma": "none",
|
5 |
+
"printWidth": 100,
|
6 |
+
"plugins": [
|
7 |
+
"prettier-plugin-svelte",
|
8 |
+
"prettier-plugin-organize-imports",
|
9 |
+
"prettier-plugin-tailwindcss"
|
10 |
+
],
|
11 |
+
"overrides": [
|
12 |
+
{
|
13 |
+
"files": "*.svelte",
|
14 |
+
"options": {
|
15 |
+
"parser": "svelte"
|
16 |
+
}
|
17 |
+
}
|
18 |
+
]
|
19 |
+
}
|
frontend/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# create-svelte
|
2 |
+
|
3 |
+
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
4 |
+
|
5 |
+
## Creating a project
|
6 |
+
|
7 |
+
If you're seeing this, you've probably already done this step. Congrats!
|
8 |
+
|
9 |
+
```bash
|
10 |
+
# create a new project in the current directory
|
11 |
+
npm create svelte@latest
|
12 |
+
|
13 |
+
# create a new project in my-app
|
14 |
+
npm create svelte@latest my-app
|
15 |
+
```
|
16 |
+
|
17 |
+
## Developing
|
18 |
+
|
19 |
+
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
20 |
+
|
21 |
+
```bash
|
22 |
+
npm run dev
|
23 |
+
|
24 |
+
# or start the server and open the app in a new browser tab
|
25 |
+
npm run dev -- --open
|
26 |
+
```
|
27 |
+
|
28 |
+
## Building
|
29 |
+
|
30 |
+
To create a production version of your app:
|
31 |
+
|
32 |
+
```bash
|
33 |
+
npm run build
|
34 |
+
```
|
35 |
+
|
36 |
+
You can preview the production build with `npm run preview`.
|
37 |
+
|
38 |
+
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
frontend/package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
frontend/package.json
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "frontend",
|
3 |
+
"version": "0.0.1",
|
4 |
+
"private": true,
|
5 |
+
"scripts": {
|
6 |
+
"dev": "vite dev",
|
7 |
+
"build": "vite build",
|
8 |
+
"preview": "vite preview",
|
9 |
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
10 |
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
11 |
+
"lint": "prettier --check . && eslint .",
|
12 |
+
"format": "prettier --write ."
|
13 |
+
},
|
14 |
+
"devDependencies": {
|
15 |
+
"@sveltejs/adapter-auto": "^2.0.0",
|
16 |
+
"@sveltejs/adapter-static": "^2.0.3",
|
17 |
+
"@sveltejs/kit": "^1.20.4",
|
18 |
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
19 |
+
"@typescript-eslint/parser": "^6.0.0",
|
20 |
+
"autoprefixer": "^10.4.16",
|
21 |
+
"eslint": "^8.28.0",
|
22 |
+
"eslint-config-prettier": "^9.0.0",
|
23 |
+
"eslint-plugin-svelte": "^2.30.0",
|
24 |
+
"postcss": "^8.4.31",
|
25 |
+
"prettier": "^3.1.0",
|
26 |
+
"prettier-plugin-organize-imports": "^3.2.4",
|
27 |
+
"prettier-plugin-svelte": "^3.1.0",
|
28 |
+
"prettier-plugin-tailwindcss": "^0.5.7",
|
29 |
+
"svelte": "^4.0.5",
|
30 |
+
"svelte-check": "^3.4.3",
|
31 |
+
"tailwindcss": "^3.3.5",
|
32 |
+
"tslib": "^2.4.1",
|
33 |
+
"typescript": "^5.0.0",
|
34 |
+
"vite": "^4.4.2"
|
35 |
+
},
|
36 |
+
"type": "module",
|
37 |
+
"dependencies": {
|
38 |
+
"piexifjs": "^1.0.6",
|
39 |
+
"rvfc-polyfill": "^1.0.7"
|
40 |
+
}
|
41 |
+
}
|
frontend/postcss.config.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export default {
|
2 |
+
plugins: {
|
3 |
+
tailwindcss: {},
|
4 |
+
autoprefixer: {}
|
5 |
+
}
|
6 |
+
};
|
frontend/src/app.css
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
@tailwind base;
|
2 |
+
@tailwind components;
|
3 |
+
@tailwind utilities;
|
frontend/src/app.d.ts
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// See https://kit.svelte.dev/docs/types#app
|
2 |
+
// for information about these interfaces
|
3 |
+
declare global {
|
4 |
+
namespace App {
|
5 |
+
// interface Error {}
|
6 |
+
// interface Locals {}
|
7 |
+
// interface PageData {}
|
8 |
+
// interface Platform {}
|
9 |
+
}
|
10 |
+
}
|
11 |
+
|
12 |
+
export {};
|
frontend/src/app.html
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!doctype html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8" />
|
5 |
+
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
7 |
+
%sveltekit.head%
|
8 |
+
</head>
|
9 |
+
<body data-sveltekit-preload-data="hover">
|
10 |
+
<div style="display: contents">%sveltekit.body%</div>
|
11 |
+
</body>
|
12 |
+
</html>
|
frontend/src/lib/components/Button.svelte
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
export let classList: string = 'p-2';
|
3 |
+
export let disabled: boolean = false;
|
4 |
+
export let title: string = '';
|
5 |
+
</script>
|
6 |
+
|
7 |
+
<button class="button {classList}" on:click {disabled} {title}>
|
8 |
+
<slot />
|
9 |
+
</button>
|
10 |
+
|
11 |
+
<style lang="postcss" scoped>
|
12 |
+
.button {
|
13 |
+
@apply rounded bg-gray-700 font-normal text-white hover:bg-gray-800 disabled:cursor-not-allowed disabled:bg-gray-300 dark:disabled:bg-gray-700 dark:disabled:text-black;
|
14 |
+
}
|
15 |
+
</style>
|
frontend/src/lib/components/Checkbox.svelte
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { FieldProps } from '$lib/types';
|
3 |
+
import { onMount } from 'svelte';
|
4 |
+
export let value = false;
|
5 |
+
export let params: FieldProps;
|
6 |
+
onMount(() => {
|
7 |
+
value = Boolean(params?.default) ?? 8.0;
|
8 |
+
});
|
9 |
+
</script>
|
10 |
+
|
11 |
+
<div class="grid max-w-md grid-cols-4 items-center justify-items-start gap-3">
|
12 |
+
<label class="text-sm font-medium" for={params.id}>{params?.title}</label>
|
13 |
+
<input bind:checked={value} type="checkbox" id={params.id} class="cursor-pointer" />
|
14 |
+
</div>
|
frontend/src/lib/components/ImagePlayer.svelte
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { lcmLiveStatus, LCMLiveStatus, streamId } from '$lib/lcmLive';
|
3 |
+
import { getPipelineValues } from '$lib/store';
|
4 |
+
|
5 |
+
import Button from '$lib/components/Button.svelte';
|
6 |
+
import Floppy from '$lib/icons/floppy.svelte';
|
7 |
+
import { snapImage } from '$lib/utils';
|
8 |
+
|
9 |
+
$: isLCMRunning = $lcmLiveStatus !== LCMLiveStatus.DISCONNECTED;
|
10 |
+
$: console.log('isLCMRunning', isLCMRunning);
|
11 |
+
let imageEl: HTMLImageElement;
|
12 |
+
async function takeSnapshot() {
|
13 |
+
if (isLCMRunning) {
|
14 |
+
await snapImage(imageEl, {
|
15 |
+
prompt: getPipelineValues()?.prompt,
|
16 |
+
negative_prompt: getPipelineValues()?.negative_prompt,
|
17 |
+
seed: getPipelineValues()?.seed,
|
18 |
+
guidance_scale: getPipelineValues()?.guidance_scale
|
19 |
+
});
|
20 |
+
}
|
21 |
+
}
|
22 |
+
</script>
|
23 |
+
|
24 |
+
<div
|
25 |
+
class="relative mx-auto aspect-square max-w-lg self-center overflow-hidden rounded-lg border border-slate-300"
|
26 |
+
>
|
27 |
+
<!-- svelte-ignore a11y-missing-attribute -->
|
28 |
+
{#if isLCMRunning && $streamId}
|
29 |
+
<img
|
30 |
+
bind:this={imageEl}
|
31 |
+
class="aspect-square w-full rounded-lg"
|
32 |
+
src={'/api/stream/' + $streamId}
|
33 |
+
/>
|
34 |
+
<div class="absolute bottom-1 right-1">
|
35 |
+
<Button
|
36 |
+
on:click={takeSnapshot}
|
37 |
+
disabled={!isLCMRunning}
|
38 |
+
title={'Take Snapshot'}
|
39 |
+
classList={'text-sm ml-auto text-white p-1 shadow-lg rounded-lg opacity-50'}
|
40 |
+
>
|
41 |
+
<Floppy classList={''} />
|
42 |
+
</Button>
|
43 |
+
</div>
|
44 |
+
{:else}
|
45 |
+
<img
|
46 |
+
class="aspect-square w-full rounded-lg"
|
47 |
+
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
|
48 |
+
/>
|
49 |
+
{/if}
|
50 |
+
</div>
|
frontend/src/lib/components/InputRange.svelte
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { FieldProps } from '$lib/types';
|
3 |
+
import { onMount } from 'svelte';
|
4 |
+
export let value = 8.0;
|
5 |
+
export let params: FieldProps;
|
6 |
+
onMount(() => {
|
7 |
+
value = Number(params?.default) ?? 8.0;
|
8 |
+
});
|
9 |
+
</script>
|
10 |
+
|
11 |
+
<div class="grid max-w-md grid-cols-4 items-center gap-3">
|
12 |
+
<label class="text-sm font-medium" for={params.id}>{params?.title}</label>
|
13 |
+
<input
|
14 |
+
class="col-span-2 h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-300 dark:bg-gray-500"
|
15 |
+
bind:value
|
16 |
+
type="range"
|
17 |
+
id={params.id}
|
18 |
+
name={params.id}
|
19 |
+
min={params?.min}
|
20 |
+
max={params?.max}
|
21 |
+
step={params?.step ?? 1}
|
22 |
+
/>
|
23 |
+
<input
|
24 |
+
type="number"
|
25 |
+
step={params?.step ?? 1}
|
26 |
+
bind:value
|
27 |
+
class="rounded-md border px-1 py-1 text-center text-xs font-bold dark:text-black"
|
28 |
+
/>
|
29 |
+
</div>
|
30 |
+
<!--
|
31 |
+
<style lang="postcss" scoped>
|
32 |
+
input[type='range']::-webkit-slider-runnable-track {
|
33 |
+
@apply h-2 cursor-pointer rounded-lg dark:bg-gray-50;
|
34 |
+
}
|
35 |
+
input[type='range']::-webkit-slider-thumb {
|
36 |
+
@apply cursor-pointer rounded-lg dark:bg-gray-50;
|
37 |
+
}
|
38 |
+
input[type='range']::-moz-range-track {
|
39 |
+
@apply cursor-pointer rounded-lg dark:bg-gray-50;
|
40 |
+
}
|
41 |
+
input[type='range']::-moz-range-thumb {
|
42 |
+
@apply cursor-pointer rounded-lg dark:bg-gray-50;
|
43 |
+
}
|
44 |
+
input[type='range']::-ms-track {
|
45 |
+
@apply cursor-pointer rounded-lg dark:bg-gray-50;
|
46 |
+
}
|
47 |
+
input[type='range']::-ms-thumb {
|
48 |
+
@apply cursor-pointer rounded-lg dark:bg-gray-50;
|
49 |
+
}
|
50 |
+
</style> -->
|
frontend/src/lib/components/MediaListSwitcher.svelte
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { mediaDevices, mediaStreamActions } from '$lib/mediaStream';
|
3 |
+
import Screen from '$lib/icons/screen.svelte';
|
4 |
+
import { onMount } from 'svelte';
|
5 |
+
|
6 |
+
let deviceId: string = '';
|
7 |
+
$: {
|
8 |
+
console.log($mediaDevices);
|
9 |
+
}
|
10 |
+
$: {
|
11 |
+
console.log(deviceId);
|
12 |
+
}
|
13 |
+
onMount(() => {
|
14 |
+
deviceId = $mediaDevices[0].deviceId;
|
15 |
+
});
|
16 |
+
</script>
|
17 |
+
|
18 |
+
<div class="flex items-center justify-center text-xs">
|
19 |
+
<button
|
20 |
+
title="Share your screen"
|
21 |
+
class="border-1 my-1 flex cursor-pointer gap-1 rounded-md border-gray-500 border-opacity-50 bg-slate-100 bg-opacity-30 p-1 font-medium text-white"
|
22 |
+
on:click={() => mediaStreamActions.startScreenCapture()}
|
23 |
+
>
|
24 |
+
<span>Share</span>
|
25 |
+
|
26 |
+
<Screen classList={''} />
|
27 |
+
</button>
|
28 |
+
{#if $mediaDevices}
|
29 |
+
<select
|
30 |
+
bind:value={deviceId}
|
31 |
+
on:change={() => mediaStreamActions.switchCamera(deviceId)}
|
32 |
+
id="devices-list"
|
33 |
+
class="border-1 block cursor-pointer rounded-md border-gray-800 border-opacity-50 bg-slate-100 bg-opacity-30 p-1 font-medium text-white"
|
34 |
+
>
|
35 |
+
{#each $mediaDevices as device, i}
|
36 |
+
<option value={device.deviceId}>{device.label}</option>
|
37 |
+
{/each}
|
38 |
+
</select>
|
39 |
+
{/if}
|
40 |
+
</div>
|
frontend/src/lib/components/PipelineOptions.svelte
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { Fields } from '$lib/types';
|
3 |
+
import { FieldType } from '$lib/types';
|
4 |
+
import InputRange from './InputRange.svelte';
|
5 |
+
import SeedInput from './SeedInput.svelte';
|
6 |
+
import TextArea from './TextArea.svelte';
|
7 |
+
import Checkbox from './Checkbox.svelte';
|
8 |
+
import Selectlist from './Selectlist.svelte';
|
9 |
+
import { pipelineValues } from '$lib/store';
|
10 |
+
|
11 |
+
export let pipelineParams: Fields;
|
12 |
+
|
13 |
+
$: advanceOptions = Object.values(pipelineParams)?.filter(
|
14 |
+
(e) => e?.hide == true && e?.disabled !== true
|
15 |
+
);
|
16 |
+
$: featuredOptions = Object.values(pipelineParams)?.filter((e) => e?.hide !== true);
|
17 |
+
</script>
|
18 |
+
|
19 |
+
<div class="flex flex-col gap-3">
|
20 |
+
<div class="grid grid-cols-1 items-center gap-3">
|
21 |
+
{#if featuredOptions}
|
22 |
+
{#each featuredOptions as params}
|
23 |
+
{#if params.field === FieldType.RANGE}
|
24 |
+
<InputRange {params} bind:value={$pipelineValues[params.id]}></InputRange>
|
25 |
+
{:else if params.field === FieldType.SEED}
|
26 |
+
<SeedInput {params} bind:value={$pipelineValues[params.id]}></SeedInput>
|
27 |
+
{:else if params.field === FieldType.TEXTAREA}
|
28 |
+
<TextArea {params} bind:value={$pipelineValues[params.id]}></TextArea>
|
29 |
+
{:else if params.field === FieldType.CHECKBOX}
|
30 |
+
<Checkbox {params} bind:value={$pipelineValues[params.id]}></Checkbox>
|
31 |
+
{:else if params.field === FieldType.SELECT}
|
32 |
+
<Selectlist {params} bind:value={$pipelineValues[params.id]}></Selectlist>
|
33 |
+
{/if}
|
34 |
+
{/each}
|
35 |
+
{/if}
|
36 |
+
</div>
|
37 |
+
{#if advanceOptions && advanceOptions.length > 0}
|
38 |
+
<details>
|
39 |
+
<summary class="cursor-pointer font-medium">Advanced Options</summary>
|
40 |
+
<div
|
41 |
+
class="grid grid-cols-1 items-center gap-3 {Object.values(pipelineParams).length > 5
|
42 |
+
? 'sm:grid-cols-2'
|
43 |
+
: ''}"
|
44 |
+
>
|
45 |
+
{#each advanceOptions as params}
|
46 |
+
{#if params.field === FieldType.RANGE}
|
47 |
+
<InputRange {params} bind:value={$pipelineValues[params.id]}></InputRange>
|
48 |
+
{:else if params.field === FieldType.SEED}
|
49 |
+
<SeedInput {params} bind:value={$pipelineValues[params.id]}></SeedInput>
|
50 |
+
{:else if params.field === FieldType.TEXTAREA}
|
51 |
+
<TextArea {params} bind:value={$pipelineValues[params.id]}></TextArea>
|
52 |
+
{:else if params.field === FieldType.CHECKBOX}
|
53 |
+
<Checkbox {params} bind:value={$pipelineValues[params.id]}></Checkbox>
|
54 |
+
{:else if params.field === FieldType.SELECT}
|
55 |
+
<Selectlist {params} bind:value={$pipelineValues[params.id]}></Selectlist>
|
56 |
+
{/if}
|
57 |
+
{/each}
|
58 |
+
</div>
|
59 |
+
</details>
|
60 |
+
{/if}
|
61 |
+
</div>
|
frontend/src/lib/components/SeedInput.svelte
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { FieldProps } from '$lib/types';
|
3 |
+
import { onMount } from 'svelte';
|
4 |
+
import Button from './Button.svelte';
|
5 |
+
export let value = 299792458;
|
6 |
+
export let params: FieldProps;
|
7 |
+
|
8 |
+
onMount(() => {
|
9 |
+
value = Number(params?.default ?? '');
|
10 |
+
});
|
11 |
+
function randomize() {
|
12 |
+
value = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
13 |
+
}
|
14 |
+
</script>
|
15 |
+
|
16 |
+
<div class="grid max-w-md grid-cols-4 items-center gap-3">
|
17 |
+
<label class="text-sm font-medium" for="seed">Seed</label>
|
18 |
+
<input
|
19 |
+
bind:value
|
20 |
+
type="number"
|
21 |
+
id="seed"
|
22 |
+
name="seed"
|
23 |
+
class="col-span-2 rounded-md border border-gray-700 p-2 text-right font-light dark:text-black"
|
24 |
+
/>
|
25 |
+
<Button on:click={randomize}>Rand</Button>
|
26 |
+
</div>
|
frontend/src/lib/components/Selectlist.svelte
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { FieldProps } from '$lib/types';
|
3 |
+
import { onMount } from 'svelte';
|
4 |
+
export let value = '';
|
5 |
+
export let params: FieldProps;
|
6 |
+
onMount(() => {
|
7 |
+
value = String(params?.default);
|
8 |
+
});
|
9 |
+
</script>
|
10 |
+
|
11 |
+
<div class="grid max-w-md grid-cols-4 items-center justify-items-start gap-3">
|
12 |
+
<label for="model-list" class="text-sm font-medium">{params?.title} </label>
|
13 |
+
{#if params?.values}
|
14 |
+
<select
|
15 |
+
bind:value
|
16 |
+
id="model-list"
|
17 |
+
class="cursor-pointer rounded-md border-2 border-gray-500 p-2 font-light dark:text-black"
|
18 |
+
>
|
19 |
+
{#each params.values as model, i}
|
20 |
+
<option value={model} selected={i === 0}>{model}</option>
|
21 |
+
{/each}
|
22 |
+
</select>
|
23 |
+
{/if}
|
24 |
+
</div>
|
frontend/src/lib/components/TextArea.svelte
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { FieldProps } from '$lib/types';
|
3 |
+
import { onMount } from 'svelte';
|
4 |
+
export let value: string;
|
5 |
+
export let params: FieldProps;
|
6 |
+
onMount(() => {
|
7 |
+
value = String(params?.default ?? '');
|
8 |
+
});
|
9 |
+
</script>
|
10 |
+
|
11 |
+
<div class="">
|
12 |
+
<label class="text-sm font-medium" for={params?.title}>
|
13 |
+
{params?.title}
|
14 |
+
</label>
|
15 |
+
<div class="text-normal flex items-center rounded-md border border-gray-700">
|
16 |
+
<textarea
|
17 |
+
class="mx-1 w-full px-3 py-2 font-light outline-none dark:text-black"
|
18 |
+
title={params?.title}
|
19 |
+
placeholder="Add your prompt here..."
|
20 |
+
bind:value
|
21 |
+
></textarea>
|
22 |
+
</div>
|
23 |
+
</div>
|
frontend/src/lib/components/VideoInput.svelte
ADDED
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import 'rvfc-polyfill';
|
3 |
+
|
4 |
+
import { onDestroy, onMount } from 'svelte';
|
5 |
+
import {
|
6 |
+
mediaStreamStatus,
|
7 |
+
MediaStreamStatusEnum,
|
8 |
+
onFrameChangeStore,
|
9 |
+
mediaStream,
|
10 |
+
mediaDevices
|
11 |
+
} from '$lib/mediaStream';
|
12 |
+
import MediaListSwitcher from './MediaListSwitcher.svelte';
|
13 |
+
export let width = 512;
|
14 |
+
export let height = 512;
|
15 |
+
const size = { width, height };
|
16 |
+
|
17 |
+
let videoEl: HTMLVideoElement;
|
18 |
+
let canvasEl: HTMLCanvasElement;
|
19 |
+
let ctx: CanvasRenderingContext2D;
|
20 |
+
let videoFrameCallbackId: number;
|
21 |
+
|
22 |
+
// ajust the throttle time to your needs
|
23 |
+
const THROTTLE = 1000 / 120;
|
24 |
+
let selectedDevice: string = '';
|
25 |
+
let videoIsReady = false;
|
26 |
+
|
27 |
+
onMount(() => {
|
28 |
+
ctx = canvasEl.getContext('2d') as CanvasRenderingContext2D;
|
29 |
+
canvasEl.width = size.width;
|
30 |
+
canvasEl.height = size.height;
|
31 |
+
});
|
32 |
+
$: {
|
33 |
+
console.log(selectedDevice);
|
34 |
+
}
|
35 |
+
onDestroy(() => {
|
36 |
+
if (videoFrameCallbackId) videoEl.cancelVideoFrameCallback(videoFrameCallbackId);
|
37 |
+
});
|
38 |
+
|
39 |
+
$: if (videoEl) {
|
40 |
+
videoEl.srcObject = $mediaStream;
|
41 |
+
}
|
42 |
+
let lastMillis = 0;
|
43 |
+
async function onFrameChange(now: DOMHighResTimeStamp, metadata: VideoFrameCallbackMetadata) {
|
44 |
+
if (now - lastMillis < THROTTLE) {
|
45 |
+
videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
|
46 |
+
return;
|
47 |
+
}
|
48 |
+
const videoWidth = videoEl.videoWidth;
|
49 |
+
const videoHeight = videoEl.videoHeight;
|
50 |
+
let height0 = videoHeight;
|
51 |
+
let width0 = videoWidth;
|
52 |
+
let x0 = 0;
|
53 |
+
let y0 = 0;
|
54 |
+
if (videoWidth > videoHeight) {
|
55 |
+
width0 = videoHeight;
|
56 |
+
x0 = (videoWidth - videoHeight) / 2;
|
57 |
+
} else {
|
58 |
+
height0 = videoWidth;
|
59 |
+
y0 = (videoHeight - videoWidth) / 2;
|
60 |
+
}
|
61 |
+
ctx.drawImage(videoEl, x0, y0, width0, height0, 0, 0, size.width, size.height);
|
62 |
+
const blob = await new Promise<Blob>((resolve) => {
|
63 |
+
canvasEl.toBlob(
|
64 |
+
(blob) => {
|
65 |
+
resolve(blob as Blob);
|
66 |
+
},
|
67 |
+
'image/jpeg',
|
68 |
+
1
|
69 |
+
);
|
70 |
+
});
|
71 |
+
onFrameChangeStore.set({ blob });
|
72 |
+
videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
|
73 |
+
}
|
74 |
+
|
75 |
+
$: if ($mediaStreamStatus == MediaStreamStatusEnum.CONNECTED && videoIsReady) {
|
76 |
+
videoFrameCallbackId = videoEl.requestVideoFrameCallback(onFrameChange);
|
77 |
+
}
|
78 |
+
</script>
|
79 |
+
|
80 |
+
<div class="relative mx-auto max-w-lg overflow-hidden rounded-lg border border-slate-300">
|
81 |
+
<div class="relative z-10 aspect-square w-full object-cover">
|
82 |
+
{#if $mediaDevices.length > 0}
|
83 |
+
<div class="absolute bottom-0 right-0 z-10">
|
84 |
+
<MediaListSwitcher />
|
85 |
+
</div>
|
86 |
+
{/if}
|
87 |
+
<video
|
88 |
+
class="pointer-events-none aspect-square w-full object-cover"
|
89 |
+
bind:this={videoEl}
|
90 |
+
on:loadeddata={() => {
|
91 |
+
videoIsReady = true;
|
92 |
+
}}
|
93 |
+
playsinline
|
94 |
+
autoplay
|
95 |
+
muted
|
96 |
+
loop
|
97 |
+
></video>
|
98 |
+
<canvas bind:this={canvasEl} class="absolute left-0 top-0 aspect-square w-full object-cover"
|
99 |
+
></canvas>
|
100 |
+
</div>
|
101 |
+
<div class="absolute left-0 top-0 flex aspect-square w-full items-center justify-center">
|
102 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 448" class="w-40 p-5 opacity-20">
|
103 |
+
<path
|
104 |
+
fill="currentColor"
|
105 |
+
d="M224 256a128 128 0 1 0 0-256 128 128 0 1 0 0 256zm-45.7 48A178.3 178.3 0 0 0 0 482.3 29.7 29.7 0 0 0 29.7 512h388.6a29.7 29.7 0 0 0 29.7-29.7c0-98.5-79.8-178.3-178.3-178.3h-91.4z"
|
106 |
+
/>
|
107 |
+
</svg>
|
108 |
+
</div>
|
109 |
+
</div>
|
frontend/src/lib/components/Warning.svelte
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
export let message: string = '';
|
3 |
+
|
4 |
+
let timeout = 0;
|
5 |
+
$: if (message !== '') {
|
6 |
+
console.log('message', message);
|
7 |
+
clearTimeout(timeout);
|
8 |
+
timeout = setTimeout(() => {
|
9 |
+
message = '';
|
10 |
+
}, 5000);
|
11 |
+
}
|
12 |
+
</script>
|
13 |
+
|
14 |
+
{#if message}
|
15 |
+
<div class="fixed right-0 top-0 m-4 cursor-pointer" on:click={() => (message = '')}>
|
16 |
+
<div class="rounded bg-red-800 p-4 text-white">
|
17 |
+
{message}
|
18 |
+
</div>
|
19 |
+
<div class="bar transition-all duration-500" style="width: 0;"></div>
|
20 |
+
</div>
|
21 |
+
{/if}
|
22 |
+
|
23 |
+
<style lang="postcss" scoped>
|
24 |
+
.button {
|
25 |
+
@apply rounded bg-gray-700 font-normal text-white hover:bg-gray-800 disabled:cursor-not-allowed disabled:bg-gray-300 dark:disabled:bg-gray-700 dark:disabled:text-black;
|
26 |
+
}
|
27 |
+
</style>
|
frontend/src/lib/icons/floppy.svelte
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
export let classList: string;
|
3 |
+
</script>
|
4 |
+
|
5 |
+
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512" class={classList}>
|
6 |
+
<path
|
7 |
+
fill="currentColor"
|
8 |
+
d="M48 96v320a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V170.5a16 16 0 0 0-4.7-11.3l33.9-33.9a64 64 0 0 1 18.7 45.3V416a64 64 0 0 1-64 64H64a64 64 0 0 1-64-64V96a64 64 0 0 1 64-64h245.5a64 64 0 0 1 45.3 18.7l74.5 74.5-33.9 33.9-74.6-74.4-.8-.8V184a24 24 0 0 1-24 24H104a24 24 0 0 1-24-24V80H64a16 16 0 0 0-16 16zm80-16v80h144V80H128zm32 240a64 64 0 1 1 128 0 64 64 0 1 1-128 0z"
|
9 |
+
/>
|
10 |
+
</svg>
|
frontend/src/lib/icons/screen.svelte
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
export let classList: string = '';
|
3 |
+
</script>
|
4 |
+
|
5 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -32 576 576" height="16px" class={classList}>
|
6 |
+
<path
|
7 |
+
fill="currentColor"
|
8 |
+
d="M64 0A64 64 0 0 0 0 64v288a64 64 0 0 0 64 64h176l-10.7 32H160a32 32 0 1 0 0 64h256a32 32 0 1 0 0-64h-69.3L336 416h176a64 64 0 0 0 64-64V64a64 64 0 0 0-64-64H64zm448 64v288H64V64h448z"
|
9 |
+
/>
|
10 |
+
</svg>
|
frontend/src/lib/icons/spinner.svelte
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
export let classList: string;
|
3 |
+
</script>
|
4 |
+
|
5 |
+
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512" class={classList}>
|
6 |
+
<path
|
7 |
+
fill="currentColor"
|
8 |
+
d="M304 48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zm0 416a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM48 304a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm464-48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM142.9 437A48 48 0 1 0 75 369.1 48 48 0 1 0 142.9 437zm0-294.2A48 48 0 1 0 75 75a48 48 0 1 0 67.9 67.9zM369.1 437A48 48 0 1 0 437 369.1 48 48 0 1 0 369.1 437z"
|
9 |
+
/>
|
10 |
+
</svg>
|
frontend/src/lib/index.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
// place files you want to import through the `$lib` alias in this folder.
|
frontend/src/lib/lcmLive.ts
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { writable } from 'svelte/store';
|
2 |
+
|
3 |
+
|
4 |
+
export enum LCMLiveStatus {
|
5 |
+
CONNECTED = "connected",
|
6 |
+
DISCONNECTED = "disconnected",
|
7 |
+
WAIT = "wait",
|
8 |
+
SEND_FRAME = "send_frame",
|
9 |
+
TIMEOUT = "timeout",
|
10 |
+
}
|
11 |
+
|
12 |
+
const initStatus: LCMLiveStatus = LCMLiveStatus.DISCONNECTED;
|
13 |
+
|
14 |
+
export const lcmLiveStatus = writable<LCMLiveStatus>(initStatus);
|
15 |
+
export const streamId = writable<string | null>(null);
|
16 |
+
|
17 |
+
let websocket: WebSocket | null = null;
|
18 |
+
export const lcmLiveActions = {
|
19 |
+
async start(getSreamdata: () => any[]) {
|
20 |
+
return new Promise((resolve, reject) => {
|
21 |
+
|
22 |
+
try {
|
23 |
+
const userId = crypto.randomUUID();
|
24 |
+
const websocketURL = `${window.location.protocol === "https:" ? "wss" : "ws"
|
25 |
+
}:${window.location.host}/api/ws/${userId}`;
|
26 |
+
|
27 |
+
websocket = new WebSocket(websocketURL);
|
28 |
+
websocket.onopen = () => {
|
29 |
+
console.log("Connected to websocket");
|
30 |
+
};
|
31 |
+
websocket.onclose = () => {
|
32 |
+
lcmLiveStatus.set(LCMLiveStatus.DISCONNECTED);
|
33 |
+
console.log("Disconnected from websocket");
|
34 |
+
};
|
35 |
+
websocket.onerror = (err) => {
|
36 |
+
console.error(err);
|
37 |
+
};
|
38 |
+
websocket.onmessage = (event) => {
|
39 |
+
const data = JSON.parse(event.data);
|
40 |
+
switch (data.status) {
|
41 |
+
case "connected":
|
42 |
+
lcmLiveStatus.set(LCMLiveStatus.CONNECTED);
|
43 |
+
streamId.set(userId);
|
44 |
+
resolve({ status: "connected", userId });
|
45 |
+
break;
|
46 |
+
case "send_frame":
|
47 |
+
lcmLiveStatus.set(LCMLiveStatus.SEND_FRAME);
|
48 |
+
const streamData = getSreamdata();
|
49 |
+
websocket?.send(JSON.stringify({ status: "next_frame" }));
|
50 |
+
for (const d of streamData) {
|
51 |
+
this.send(d);
|
52 |
+
}
|
53 |
+
break;
|
54 |
+
case "wait":
|
55 |
+
lcmLiveStatus.set(LCMLiveStatus.WAIT);
|
56 |
+
break;
|
57 |
+
case "timeout":
|
58 |
+
console.log("timeout");
|
59 |
+
lcmLiveStatus.set(LCMLiveStatus.TIMEOUT);
|
60 |
+
streamId.set(null);
|
61 |
+
reject(new Error("timeout"));
|
62 |
+
break;
|
63 |
+
case "error":
|
64 |
+
console.log(data.message);
|
65 |
+
lcmLiveStatus.set(LCMLiveStatus.DISCONNECTED);
|
66 |
+
streamId.set(null);
|
67 |
+
reject(new Error(data.message));
|
68 |
+
break;
|
69 |
+
}
|
70 |
+
};
|
71 |
+
|
72 |
+
} catch (err) {
|
73 |
+
console.error(err);
|
74 |
+
lcmLiveStatus.set(LCMLiveStatus.DISCONNECTED);
|
75 |
+
streamId.set(null);
|
76 |
+
reject(err);
|
77 |
+
}
|
78 |
+
});
|
79 |
+
},
|
80 |
+
send(data: Blob | { [key: string]: any }) {
|
81 |
+
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
82 |
+
if (data instanceof Blob) {
|
83 |
+
websocket.send(data);
|
84 |
+
} else {
|
85 |
+
websocket.send(JSON.stringify(data));
|
86 |
+
}
|
87 |
+
} else {
|
88 |
+
console.log("WebSocket not connected");
|
89 |
+
}
|
90 |
+
},
|
91 |
+
async stop() {
|
92 |
+
lcmLiveStatus.set(LCMLiveStatus.DISCONNECTED);
|
93 |
+
if (websocket) {
|
94 |
+
websocket.close();
|
95 |
+
}
|
96 |
+
websocket = null;
|
97 |
+
streamId.set(null);
|
98 |
+
},
|
99 |
+
};
|
frontend/src/lib/mediaStream.ts
ADDED
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { writable, type Writable, get } from 'svelte/store';
|
2 |
+
|
3 |
+
export enum MediaStreamStatusEnum {
|
4 |
+
INIT = "init",
|
5 |
+
CONNECTED = "connected",
|
6 |
+
DISCONNECTED = "disconnected",
|
7 |
+
}
|
8 |
+
export const onFrameChangeStore: Writable<{ blob: Blob }> = writable({ blob: new Blob() });
|
9 |
+
|
10 |
+
export const mediaDevices = writable<MediaDeviceInfo[]>([]);
|
11 |
+
export const mediaStreamStatus = writable(MediaStreamStatusEnum.INIT);
|
12 |
+
export const mediaStream = writable<MediaStream | null>(null);
|
13 |
+
|
14 |
+
export const mediaStreamActions = {
|
15 |
+
async enumerateDevices() {
|
16 |
+
// console.log("Enumerating devices");
|
17 |
+
await navigator.mediaDevices.enumerateDevices()
|
18 |
+
.then(devices => {
|
19 |
+
const cameras = devices.filter(device => device.kind === 'videoinput');
|
20 |
+
mediaDevices.set(cameras);
|
21 |
+
})
|
22 |
+
.catch(err => {
|
23 |
+
console.error(err);
|
24 |
+
});
|
25 |
+
},
|
26 |
+
async start(mediaDevicedID?: string) {
|
27 |
+
const constraints = {
|
28 |
+
audio: false,
|
29 |
+
video: {
|
30 |
+
width: 1024, height: 1024, deviceId: mediaDevicedID
|
31 |
+
}
|
32 |
+
};
|
33 |
+
|
34 |
+
await navigator.mediaDevices
|
35 |
+
.getUserMedia(constraints)
|
36 |
+
.then((stream) => {
|
37 |
+
mediaStreamStatus.set(MediaStreamStatusEnum.CONNECTED);
|
38 |
+
mediaStream.set(stream);
|
39 |
+
})
|
40 |
+
.catch((err) => {
|
41 |
+
console.error(`${err.name}: ${err.message}`);
|
42 |
+
mediaStreamStatus.set(MediaStreamStatusEnum.DISCONNECTED);
|
43 |
+
mediaStream.set(null);
|
44 |
+
});
|
45 |
+
},
|
46 |
+
async startScreenCapture() {
|
47 |
+
const displayMediaOptions = {
|
48 |
+
video: {
|
49 |
+
displaySurface: "window",
|
50 |
+
},
|
51 |
+
audio: false,
|
52 |
+
surfaceSwitching: "include"
|
53 |
+
};
|
54 |
+
|
55 |
+
|
56 |
+
let captureStream = null;
|
57 |
+
|
58 |
+
try {
|
59 |
+
captureStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
|
60 |
+
const videoTrack = captureStream.getVideoTracks()[0];
|
61 |
+
|
62 |
+
console.log("Track settings:");
|
63 |
+
console.log(JSON.stringify(videoTrack.getSettings(), null, 2));
|
64 |
+
console.log("Track constraints:");
|
65 |
+
console.log(JSON.stringify(videoTrack.getConstraints(), null, 2));
|
66 |
+
mediaStreamStatus.set(MediaStreamStatusEnum.CONNECTED);
|
67 |
+
mediaStream.set(captureStream)
|
68 |
+
} catch (err) {
|
69 |
+
console.error(err);
|
70 |
+
}
|
71 |
+
|
72 |
+
},
|
73 |
+
async switchCamera(mediaDevicedID: string) {
|
74 |
+
if (get(mediaStreamStatus) !== MediaStreamStatusEnum.CONNECTED) {
|
75 |
+
return;
|
76 |
+
}
|
77 |
+
const constraints = {
|
78 |
+
audio: false,
|
79 |
+
video: { width: 1024, height: 1024, deviceId: mediaDevicedID }
|
80 |
+
};
|
81 |
+
await navigator.mediaDevices
|
82 |
+
.getUserMedia(constraints)
|
83 |
+
.then((stream) => {
|
84 |
+
mediaStreamStatus.set(MediaStreamStatusEnum.CONNECTED);
|
85 |
+
mediaStream.set(stream)
|
86 |
+
})
|
87 |
+
.catch((err) => {
|
88 |
+
console.error(`${err.name}: ${err.message}`);
|
89 |
+
});
|
90 |
+
},
|
91 |
+
async stop() {
|
92 |
+
navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => {
|
93 |
+
stream.getTracks().forEach((track) => track.stop());
|
94 |
+
});
|
95 |
+
mediaStreamStatus.set(MediaStreamStatusEnum.DISCONNECTED);
|
96 |
+
mediaStream.set(null);
|
97 |
+
},
|
98 |
+
};
|
frontend/src/lib/store.ts
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import { derived, writable, get, type Writable, type Readable } from 'svelte/store';
|
3 |
+
|
4 |
+
export const pipelineValues: Writable<Record<string, any>> = writable({});
|
5 |
+
export const deboucedPipelineValues: Readable<Record<string, any>>
|
6 |
+
= derived(pipelineValues, ($pipelineValues, set) => {
|
7 |
+
const debounced = setTimeout(() => {
|
8 |
+
set($pipelineValues);
|
9 |
+
}, 100);
|
10 |
+
return () => clearTimeout(debounced);
|
11 |
+
});
|
12 |
+
|
13 |
+
|
14 |
+
|
15 |
+
export const getPipelineValues = () => get(pipelineValues);
|
frontend/src/lib/types.ts
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const enum FieldType {
|
2 |
+
RANGE = "range",
|
3 |
+
SEED = "seed",
|
4 |
+
TEXTAREA = "textarea",
|
5 |
+
CHECKBOX = "checkbox",
|
6 |
+
SELECT = "select",
|
7 |
+
}
|
8 |
+
export const enum PipelineMode {
|
9 |
+
IMAGE = "image",
|
10 |
+
VIDEO = "video",
|
11 |
+
TEXT = "text",
|
12 |
+
}
|
13 |
+
|
14 |
+
|
15 |
+
export interface Fields {
|
16 |
+
[key: string]: FieldProps;
|
17 |
+
}
|
18 |
+
|
19 |
+
export interface FieldProps {
|
20 |
+
default: number | string;
|
21 |
+
max?: number;
|
22 |
+
min?: number;
|
23 |
+
title: string;
|
24 |
+
field: FieldType;
|
25 |
+
step?: number;
|
26 |
+
disabled?: boolean;
|
27 |
+
hide?: boolean;
|
28 |
+
id: string;
|
29 |
+
values?: string[];
|
30 |
+
}
|
31 |
+
export interface PipelineInfo {
|
32 |
+
title: {
|
33 |
+
default: string;
|
34 |
+
}
|
35 |
+
name: string;
|
36 |
+
description: string;
|
37 |
+
input_mode: {
|
38 |
+
default: PipelineMode;
|
39 |
+
}
|
40 |
+
}
|
frontend/src/lib/utils.ts
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as piexif from "piexifjs";
|
2 |
+
|
3 |
+
interface IImageInfo {
|
4 |
+
prompt?: string;
|
5 |
+
negative_prompt?: string;
|
6 |
+
seed?: number;
|
7 |
+
guidance_scale?: number;
|
8 |
+
}
|
9 |
+
|
10 |
+
export function snapImage(imageEl: HTMLImageElement, info: IImageInfo) {
|
11 |
+
try {
|
12 |
+
const zeroth: { [key: string]: any } = {};
|
13 |
+
const exif: { [key: string]: any } = {};
|
14 |
+
const gps: { [key: string]: any } = {};
|
15 |
+
zeroth[piexif.ImageIFD.Make] = "LCM Image-to-Image ControNet";
|
16 |
+
zeroth[piexif.ImageIFD.ImageDescription] = `prompt: ${info?.prompt} | negative_prompt: ${info?.negative_prompt} | seed: ${info?.seed} | guidance_scale: ${info?.guidance_scale}`;
|
17 |
+
zeroth[piexif.ImageIFD.Software] = "https://github.com/radames/Real-Time-Latent-Consistency-Model";
|
18 |
+
exif[piexif.ExifIFD.DateTimeOriginal] = new Date().toISOString();
|
19 |
+
|
20 |
+
const exifObj = { "0th": zeroth, "Exif": exif, "GPS": gps };
|
21 |
+
const exifBytes = piexif.dump(exifObj);
|
22 |
+
|
23 |
+
const canvas = document.createElement("canvas");
|
24 |
+
canvas.width = imageEl.naturalWidth;
|
25 |
+
canvas.height = imageEl.naturalHeight;
|
26 |
+
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
27 |
+
ctx.drawImage(imageEl, 0, 0);
|
28 |
+
const dataURL = canvas.toDataURL("image/jpeg");
|
29 |
+
const withExif = piexif.insert(exifBytes, dataURL);
|
30 |
+
|
31 |
+
const a = document.createElement("a");
|
32 |
+
a.href = withExif;
|
33 |
+
a.download = `lcm_txt_2_img${Date.now()}.png`;
|
34 |
+
a.click();
|
35 |
+
} catch (err) {
|
36 |
+
console.log(err);
|
37 |
+
}
|
38 |
+
}
|
frontend/src/routes/+layout.svelte
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script>
|
2 |
+
import '../app.css';
|
3 |
+
</script>
|
4 |
+
|
5 |
+
<slot />
|
frontend/src/routes/+page.svelte
ADDED
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { onMount } from 'svelte';
|
3 |
+
import type { Fields, PipelineInfo } from '$lib/types';
|
4 |
+
import { PipelineMode } from '$lib/types';
|
5 |
+
import ImagePlayer from '$lib/components/ImagePlayer.svelte';
|
6 |
+
import VideoInput from '$lib/components/VideoInput.svelte';
|
7 |
+
import Button from '$lib/components/Button.svelte';
|
8 |
+
import PipelineOptions from '$lib/components/PipelineOptions.svelte';
|
9 |
+
import Spinner from '$lib/icons/spinner.svelte';
|
10 |
+
import Warning from '$lib/components/Warning.svelte';
|
11 |
+
import { lcmLiveStatus, lcmLiveActions, LCMLiveStatus } from '$lib/lcmLive';
|
12 |
+
import { mediaStreamActions, onFrameChangeStore } from '$lib/mediaStream';
|
13 |
+
import { getPipelineValues, deboucedPipelineValues } from '$lib/store';
|
14 |
+
|
15 |
+
let pipelineParams: Fields;
|
16 |
+
let pipelineInfo: PipelineInfo;
|
17 |
+
let pageContent: string;
|
18 |
+
let isImageMode: boolean = false;
|
19 |
+
let maxQueueSize: number = 0;
|
20 |
+
let currentQueueSize: number = 0;
|
21 |
+
let queueCheckerRunning: boolean = false;
|
22 |
+
let warningMessage: string = '';
|
23 |
+
onMount(() => {
|
24 |
+
getSettings();
|
25 |
+
});
|
26 |
+
|
27 |
+
async function getSettings() {
|
28 |
+
const settings = await fetch('/api/settings').then((r) => r.json());
|
29 |
+
pipelineParams = settings.input_params.properties;
|
30 |
+
pipelineInfo = settings.info.properties;
|
31 |
+
isImageMode = pipelineInfo.input_mode.default === PipelineMode.IMAGE;
|
32 |
+
maxQueueSize = settings.max_queue_size;
|
33 |
+
pageContent = settings.page_content;
|
34 |
+
console.log(pipelineParams);
|
35 |
+
toggleQueueChecker(true);
|
36 |
+
}
|
37 |
+
function toggleQueueChecker(start: boolean) {
|
38 |
+
queueCheckerRunning = start && maxQueueSize > 0;
|
39 |
+
if (start) {
|
40 |
+
getQueueSize();
|
41 |
+
}
|
42 |
+
}
|
43 |
+
async function getQueueSize() {
|
44 |
+
if (!queueCheckerRunning) {
|
45 |
+
return;
|
46 |
+
}
|
47 |
+
const data = await fetch('/api/queue').then((r) => r.json());
|
48 |
+
currentQueueSize = data.queue_size;
|
49 |
+
setTimeout(getQueueSize, 10000);
|
50 |
+
}
|
51 |
+
|
52 |
+
function getSreamdata() {
|
53 |
+
if (isImageMode) {
|
54 |
+
return [getPipelineValues(), $onFrameChangeStore?.blob];
|
55 |
+
} else {
|
56 |
+
return [$deboucedPipelineValues];
|
57 |
+
}
|
58 |
+
}
|
59 |
+
|
60 |
+
$: isLCMRunning = $lcmLiveStatus !== LCMLiveStatus.DISCONNECTED;
|
61 |
+
$: if ($lcmLiveStatus === LCMLiveStatus.TIMEOUT) {
|
62 |
+
warningMessage = 'Session timed out. Please try again.';
|
63 |
+
}
|
64 |
+
let disabled = false;
|
65 |
+
async function toggleLcmLive() {
|
66 |
+
try {
|
67 |
+
if (!isLCMRunning) {
|
68 |
+
if (isImageMode) {
|
69 |
+
await mediaStreamActions.enumerateDevices();
|
70 |
+
await mediaStreamActions.start();
|
71 |
+
}
|
72 |
+
disabled = true;
|
73 |
+
await lcmLiveActions.start(getSreamdata);
|
74 |
+
disabled = false;
|
75 |
+
toggleQueueChecker(false);
|
76 |
+
} else {
|
77 |
+
if (isImageMode) {
|
78 |
+
mediaStreamActions.stop();
|
79 |
+
}
|
80 |
+
lcmLiveActions.stop();
|
81 |
+
toggleQueueChecker(true);
|
82 |
+
}
|
83 |
+
} catch (e) {
|
84 |
+
warningMessage = e instanceof Error ? e.message : '';
|
85 |
+
disabled = false;
|
86 |
+
toggleQueueChecker(true);
|
87 |
+
}
|
88 |
+
}
|
89 |
+
</script>
|
90 |
+
|
91 |
+
<svelte:head>
|
92 |
+
<script
|
93 |
+
src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.9/iframeResizer.contentWindow.min.js"
|
94 |
+
></script>
|
95 |
+
</svelte:head>
|
96 |
+
|
97 |
+
<main class="container mx-auto flex max-w-5xl flex-col gap-3 px-4 py-4">
|
98 |
+
<Warning bind:message={warningMessage}></Warning>
|
99 |
+
<article class="text-center">
|
100 |
+
{#if pageContent}
|
101 |
+
{@html pageContent}
|
102 |
+
{/if}
|
103 |
+
{#if maxQueueSize > 0}
|
104 |
+
<p class="text-sm">
|
105 |
+
There are <span id="queue_size" class="font-bold">{currentQueueSize}</span>
|
106 |
+
user(s) sharing the same GPU, affecting real-time performance. Maximum queue size is {maxQueueSize}.
|
107 |
+
<a
|
108 |
+
href="https://huggingface.co/spaces/radames/Real-Time-Latent-Consistency-Model?duplicate=true"
|
109 |
+
target="_blank"
|
110 |
+
class="text-blue-500 underline hover:no-underline">Duplicate</a
|
111 |
+
> and run it on your own GPU.
|
112 |
+
</p>
|
113 |
+
{/if}
|
114 |
+
</article>
|
115 |
+
{#if pipelineParams}
|
116 |
+
<article class="my-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
117 |
+
{#if isImageMode}
|
118 |
+
<div class="sm:col-start-1">
|
119 |
+
<VideoInput
|
120 |
+
width={Number(pipelineParams.width.default)}
|
121 |
+
height={Number(pipelineParams.height.default)}
|
122 |
+
></VideoInput>
|
123 |
+
</div>
|
124 |
+
{/if}
|
125 |
+
<div class={isImageMode ? 'sm:col-start-2' : 'col-span-2'}>
|
126 |
+
<ImagePlayer />
|
127 |
+
</div>
|
128 |
+
<div class="sm:col-span-2">
|
129 |
+
<Button on:click={toggleLcmLive} {disabled} classList={'text-lg my-1 p-2'}>
|
130 |
+
{#if isLCMRunning}
|
131 |
+
Stop
|
132 |
+
{:else}
|
133 |
+
Start
|
134 |
+
{/if}
|
135 |
+
</Button>
|
136 |
+
<PipelineOptions {pipelineParams}></PipelineOptions>
|
137 |
+
</div>
|
138 |
+
</article>
|
139 |
+
{:else}
|
140 |
+
<!-- loading -->
|
141 |
+
<div class="flex items-center justify-center gap-3 py-48 text-2xl">
|
142 |
+
<Spinner classList={'animate-spin opacity-50'}></Spinner>
|
143 |
+
<p>Loading...</p>
|
144 |
+
</div>
|
145 |
+
{/if}
|
146 |
+
</main>
|
147 |
+
|
148 |
+
<style lang="postcss">
|
149 |
+
:global(html) {
|
150 |
+
@apply text-black dark:bg-gray-900 dark:text-white;
|
151 |
+
}
|
152 |
+
</style>
|
frontend/src/routes/+page.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export const prerender = true
|
frontend/static/favicon.png
ADDED
![]() |
frontend/svelte.config.js
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import adapter from '@sveltejs/adapter-static';
|
2 |
+
import { vitePreprocess } from '@sveltejs/kit/vite';
|
3 |
+
/** @type {import('@sveltejs/kit').Config} */
|
4 |
+
const config = {
|
5 |
+
preprocess: vitePreprocess({ postcss: true }),
|
6 |
+
kit: {
|
7 |
+
adapter: adapter({
|
8 |
+
pages: 'public',
|
9 |
+
assets: 'public',
|
10 |
+
fallback: undefined,
|
11 |
+
precompress: false,
|
12 |
+
strict: true
|
13 |
+
})
|
14 |
+
}
|
15 |
+
};
|
16 |
+
|
17 |
+
export default config;
|
frontend/tailwind.config.js
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import('tailwindcss').Config} */
|
2 |
+
export default {
|
3 |
+
content: ['./src/**/*.{html,js,svelte,ts}', '../**/*.py'],
|
4 |
+
theme: {
|
5 |
+
extend: {}
|
6 |
+
},
|
7 |
+
plugins: []
|
8 |
+
};
|
frontend/tsconfig.json
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"extends": "./.svelte-kit/tsconfig.json",
|
3 |
+
"compilerOptions": {
|
4 |
+
"allowJs": true,
|
5 |
+
"checkJs": true,
|
6 |
+
"esModuleInterop": true,
|
7 |
+
"forceConsistentCasingInFileNames": true,
|
8 |
+
"resolveJsonModule": true,
|
9 |
+
"skipLibCheck": true,
|
10 |
+
"sourceMap": true,
|
11 |
+
"strict": true
|
12 |
+
}
|
13 |
+
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
14 |
+
//
|
15 |
+
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
16 |
+
// from the referenced tsconfig.json - TypeScript does not merge them in
|
17 |
+
}
|
frontend/vite.config.ts
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { sveltekit } from '@sveltejs/kit/vite';
|
2 |
+
import { defineConfig } from 'vite';
|
3 |
+
|
4 |
+
export default defineConfig({
|
5 |
+
plugins: [sveltekit()],
|
6 |
+
server: {
|
7 |
+
proxy: {
|
8 |
+
'/api': 'http://localhost:7860',
|
9 |
+
'/api/ws': {
|
10 |
+
target: 'ws://localhost:7860',
|
11 |
+
ws: true
|
12 |
+
}
|
13 |
+
},
|
14 |
+
}
|
15 |
+
});
|
main.py
ADDED
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, WebSocket, HTTPException, WebSocketDisconnect
|
2 |
+
from fastapi.responses import StreamingResponse, JSONResponse
|
3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
4 |
+
from fastapi.staticfiles import StaticFiles
|
5 |
+
from fastapi import Request
|
6 |
+
|
7 |
+
import markdown2
|
8 |
+
|
9 |
+
import logging
|
10 |
+
import uuid
|
11 |
+
import time
|
12 |
+
from types import SimpleNamespace
|
13 |
+
import asyncio
|
14 |
+
import os
|
15 |
+
import time
|
16 |
+
import mimetypes
|
17 |
+
import torch
|
18 |
+
|
19 |
+
from config import config, Args
|
20 |
+
from util import pil_to_frame, bytes_to_pil, is_firefox
|
21 |
+
from connection_manager import ConnectionManager, ServerFullException
|
22 |
+
from vid2vid import Pipeline
|
23 |
+
|
24 |
+
# fix mime error on windows
|
25 |
+
mimetypes.add_type("application/javascript", ".js")
|
26 |
+
|
27 |
+
THROTTLE = 1.0 / 120
|
28 |
+
# logging.basicConfig(level=logging.DEBUG)
|
29 |
+
|
30 |
+
|
31 |
+
class App:
|
32 |
+
def __init__(self, config: Args, pipeline):
|
33 |
+
self.args = config
|
34 |
+
self.pipeline = pipeline
|
35 |
+
self.app = FastAPI()
|
36 |
+
self.conn_manager = ConnectionManager()
|
37 |
+
self.init_app()
|
38 |
+
|
39 |
+
def init_app(self):
|
40 |
+
self.app.add_middleware(
|
41 |
+
CORSMiddleware,
|
42 |
+
allow_origins=["*"],
|
43 |
+
allow_credentials=True,
|
44 |
+
allow_methods=["*"],
|
45 |
+
allow_headers=["*"],
|
46 |
+
)
|
47 |
+
|
48 |
+
@self.app.websocket("/api/ws/{user_id}")
|
49 |
+
async def websocket_endpoint(user_id: uuid.UUID, websocket: WebSocket):
|
50 |
+
try:
|
51 |
+
await self.conn_manager.connect(
|
52 |
+
user_id, websocket, self.args.max_queue_size
|
53 |
+
)
|
54 |
+
await handle_websocket_data(user_id)
|
55 |
+
except ServerFullException as e:
|
56 |
+
logging.error(f"Server Full: {e}")
|
57 |
+
finally:
|
58 |
+
await self.conn_manager.disconnect(user_id)
|
59 |
+
logging.info(f"User disconnected: {user_id}")
|
60 |
+
|
61 |
+
async def handle_websocket_data(user_id: uuid.UUID):
|
62 |
+
if not self.conn_manager.check_user(user_id):
|
63 |
+
return HTTPException(status_code=404, detail="User not found")
|
64 |
+
last_time = time.time()
|
65 |
+
try:
|
66 |
+
while True:
|
67 |
+
if (
|
68 |
+
self.args.timeout > 0
|
69 |
+
and time.time() - last_time > self.args.timeout
|
70 |
+
):
|
71 |
+
await self.conn_manager.send_json(
|
72 |
+
user_id,
|
73 |
+
{
|
74 |
+
"status": "timeout",
|
75 |
+
"message": "Your session has ended",
|
76 |
+
},
|
77 |
+
)
|
78 |
+
await self.conn_manager.disconnect(user_id)
|
79 |
+
return
|
80 |
+
data = await self.conn_manager.receive_json(user_id)
|
81 |
+
if data["status"] != "next_frame":
|
82 |
+
asyncio.sleep(THROTTLE)
|
83 |
+
continue
|
84 |
+
|
85 |
+
params = await self.conn_manager.receive_json(user_id)
|
86 |
+
params = pipeline.InputParams(**params)
|
87 |
+
info = pipeline.Info()
|
88 |
+
params = SimpleNamespace(**params.dict())
|
89 |
+
if info.input_mode == "image":
|
90 |
+
image_data = await self.conn_manager.receive_bytes(user_id)
|
91 |
+
if len(image_data) == 0:
|
92 |
+
await self.conn_manager.send_json(
|
93 |
+
user_id, {"status": "send_frame"}
|
94 |
+
)
|
95 |
+
await asyncio.sleep(THROTTLE)
|
96 |
+
continue
|
97 |
+
params.image = bytes_to_pil(image_data)
|
98 |
+
await self.conn_manager.update_data(user_id, params)
|
99 |
+
await self.conn_manager.send_json(user_id, {"status": "wait"})
|
100 |
+
|
101 |
+
except Exception as e:
|
102 |
+
logging.error(f"Websocket Error: {e}, {user_id} ")
|
103 |
+
await self.conn_manager.disconnect(user_id)
|
104 |
+
|
105 |
+
@self.app.get("/api/queue")
|
106 |
+
async def get_queue_size():
|
107 |
+
queue_size = self.conn_manager.get_user_count()
|
108 |
+
return JSONResponse({"queue_size": queue_size})
|
109 |
+
|
110 |
+
@self.app.get("/api/stream/{user_id}")
|
111 |
+
async def stream(user_id: uuid.UUID, request: Request):
|
112 |
+
try:
|
113 |
+
|
114 |
+
async def generate():
|
115 |
+
last_params = SimpleNamespace()
|
116 |
+
while True:
|
117 |
+
last_time = time.time()
|
118 |
+
params = await self.conn_manager.get_latest_data(user_id)
|
119 |
+
if not vars(params) or params.__dict__ == last_params.__dict__:
|
120 |
+
await self.conn_manager.send_json(
|
121 |
+
user_id, {"status": "send_frame"}
|
122 |
+
)
|
123 |
+
continue
|
124 |
+
|
125 |
+
last_params = params
|
126 |
+
image = pipeline.predict(params)
|
127 |
+
if image is None:
|
128 |
+
await self.conn_manager.send_json(
|
129 |
+
user_id, {"status": "send_frame"}
|
130 |
+
)
|
131 |
+
continue
|
132 |
+
frame = pil_to_frame(image)
|
133 |
+
yield frame
|
134 |
+
# https://bugs.chromium.org/p/chromium/issues/detail?id=1250396
|
135 |
+
if not is_firefox(request.headers["user-agent"]):
|
136 |
+
yield frame
|
137 |
+
await self.conn_manager.send_json(
|
138 |
+
user_id, {"status": "send_frame"}
|
139 |
+
)
|
140 |
+
if self.args.debug:
|
141 |
+
print(f"Time taken: {time.time() - last_time}")
|
142 |
+
|
143 |
+
return StreamingResponse(
|
144 |
+
generate(),
|
145 |
+
media_type="multipart/x-mixed-replace;boundary=frame",
|
146 |
+
headers={"Cache-Control": "no-cache"},
|
147 |
+
)
|
148 |
+
except Exception as e:
|
149 |
+
logging.error(f"Streaming Error: {e}, {user_id} ")
|
150 |
+
return HTTPException(status_code=404, detail="User not found")
|
151 |
+
|
152 |
+
# route to setup frontend
|
153 |
+
@self.app.get("/api/settings")
|
154 |
+
async def settings():
|
155 |
+
info_schema = pipeline.Info.schema()
|
156 |
+
info = pipeline.Info()
|
157 |
+
if info.page_content:
|
158 |
+
page_content = markdown2.markdown(info.page_content)
|
159 |
+
|
160 |
+
input_params = pipeline.InputParams.schema()
|
161 |
+
return JSONResponse(
|
162 |
+
{
|
163 |
+
"info": info_schema,
|
164 |
+
"input_params": input_params,
|
165 |
+
"max_queue_size": self.args.max_queue_size,
|
166 |
+
"page_content": page_content if info.page_content else "",
|
167 |
+
}
|
168 |
+
)
|
169 |
+
|
170 |
+
if not os.path.exists("./frontend/public"):
|
171 |
+
os.makedirs("./frontend/public")
|
172 |
+
|
173 |
+
self.app.mount(
|
174 |
+
"/", StaticFiles(directory="./frontend/public", html=True), name="public"
|
175 |
+
)
|
176 |
+
|
177 |
+
|
178 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
179 |
+
torch_dtype = torch.float16
|
180 |
+
pipeline = Pipeline(config, device, torch_dtype)
|
181 |
+
app = App(config, pipeline).app
|
182 |
+
|
183 |
+
if __name__ == "__main__":
|
184 |
+
import uvicorn
|
185 |
+
|
186 |
+
uvicorn.run(
|
187 |
+
"main:app",
|
188 |
+
host=config.host,
|
189 |
+
port=config.port,
|
190 |
+
reload=config.reload,
|
191 |
+
ssl_certfile=config.ssl_certfile,
|
192 |
+
ssl_keyfile=config.ssl_keyfile,
|
193 |
+
)
|
requirements.txt
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
diffusers==0.27.0
|
2 |
+
transformers==4.35.2
|
3 |
+
--extra-index-url https://download.pytorch.org/whl/cu121;
|
4 |
+
torch
|
5 |
+
fastapi==0.104.1
|
6 |
+
uvicorn[standard]==0.24.0.post1
|
7 |
+
Pillow==10.1.0
|
8 |
+
accelerate==0.24.0
|
9 |
+
compel==2.0.2
|
10 |
+
controlnet-aux==0.0.7
|
11 |
+
peft==0.6.0
|
12 |
+
xformers; sys_platform != 'darwin' or platform_machine != 'arm64'
|
13 |
+
markdown2
|
14 |
+
stable_fast @ https://github.com/chengzeyi/stable-fast/releases/download/v0.0.15.post1/stable_fast-0.0.15.post1+torch211cu121-cp310-cp310-manylinux2014_x86_64.whl; sys_platform=='linux'
|
start.sh
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
cd frontend
|
3 |
+
npm install
|
4 |
+
npm run build
|
5 |
+
if [ $? -eq 0 ]; then
|
6 |
+
echo -e "\033[1;32m\nfrontend build success \033[0m"
|
7 |
+
else
|
8 |
+
echo -e "\033[1;31m\nfrontend build failed\n\033[0m" >&2 exit 1
|
9 |
+
fi
|
10 |
+
cd ../
|
11 |
+
python3 main.py --port 7860 --host 0.0.0.0
|