jbilcke-hf HF Staff commited on
Commit
51669fc
·
0 Parent(s):

original code by JeffLiang and Radames

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .DS_Store +0 -0
  2. Dockerfile +48 -0
  3. LICENSE +53 -0
  4. README.md +17 -0
  5. config.py +109 -0
  6. connection_manager.py +116 -0
  7. frontend/.eslintignore +13 -0
  8. frontend/.eslintrc.cjs +30 -0
  9. frontend/.gitignore +10 -0
  10. frontend/.npmrc +1 -0
  11. frontend/.prettierignore +13 -0
  12. frontend/.prettierrc +19 -0
  13. frontend/README.md +38 -0
  14. frontend/package-lock.json +0 -0
  15. frontend/package.json +41 -0
  16. frontend/postcss.config.js +6 -0
  17. frontend/src/app.css +3 -0
  18. frontend/src/app.d.ts +12 -0
  19. frontend/src/app.html +12 -0
  20. frontend/src/lib/components/Button.svelte +15 -0
  21. frontend/src/lib/components/Checkbox.svelte +14 -0
  22. frontend/src/lib/components/ImagePlayer.svelte +50 -0
  23. frontend/src/lib/components/InputRange.svelte +50 -0
  24. frontend/src/lib/components/MediaListSwitcher.svelte +40 -0
  25. frontend/src/lib/components/PipelineOptions.svelte +61 -0
  26. frontend/src/lib/components/SeedInput.svelte +26 -0
  27. frontend/src/lib/components/Selectlist.svelte +24 -0
  28. frontend/src/lib/components/TextArea.svelte +23 -0
  29. frontend/src/lib/components/VideoInput.svelte +109 -0
  30. frontend/src/lib/components/Warning.svelte +27 -0
  31. frontend/src/lib/icons/floppy.svelte +10 -0
  32. frontend/src/lib/icons/screen.svelte +10 -0
  33. frontend/src/lib/icons/spinner.svelte +10 -0
  34. frontend/src/lib/index.ts +1 -0
  35. frontend/src/lib/lcmLive.ts +99 -0
  36. frontend/src/lib/mediaStream.ts +98 -0
  37. frontend/src/lib/store.ts +15 -0
  38. frontend/src/lib/types.ts +40 -0
  39. frontend/src/lib/utils.ts +38 -0
  40. frontend/src/routes/+layout.svelte +5 -0
  41. frontend/src/routes/+page.svelte +152 -0
  42. frontend/src/routes/+page.ts +1 -0
  43. frontend/static/favicon.png +0 -0
  44. frontend/svelte.config.js +17 -0
  45. frontend/tailwind.config.js +8 -0
  46. frontend/tsconfig.json +17 -0
  47. frontend/vite.config.ts +15 -0
  48. main.py +193 -0
  49. requirements.txt +14 -0
  50. 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