diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..218354d --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,11 @@ +__pycache__ + +migrations + +.env +.env.dev +.env.prod +data/ +*.conf +.sh +certbot/ \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..d301204 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,19 @@ +FROM python:latest + +RUN mkdir /backend +WORKDIR /backend + +COPY . /backend/ + +RUN apt-get update && apt-get install -y ffmpeg +RUN pip install --upgrade pip +RUN pip install -r requirements.txt + +# Set environment variables for ffmpeg and ffprobe +ENV FFMPEG_PATH /usr/bin/ffmpeg +ENV FFPROBE_PATH /usr/bin/ffprobe + +# for error message +ENV PYTHONUNBUFFERED 1 + +# CMD ["flask", "run", "--host=0.0.0.0", "--port=5001"] \ No newline at end of file diff --git a/backend/apis_v1/MzRequest.py b/backend/apis_v1/MzRequest.py new file mode 100644 index 0000000..b1a5a03 --- /dev/null +++ b/backend/apis_v1/MzRequest.py @@ -0,0 +1,245 @@ +from flask import Response +import json +from module import crud_module +from flask_restx import Resource, Namespace +from werkzeug.datastructures import FileStorage + +####################################생성 전 정보####################################### + +MzRequest = Namespace( + name="MzRequest", + description="MzRequest CRUD를 작성하기 위해 사용하는 API.", +) + +common_parser = MzRequest.parser() +common_parser.add_argument('token', location='headers') + +post_parser = common_parser.copy() +post_parser.add_argument('age', location='form', required=False) +post_parser.add_argument('gender', location='form', required=False) +post_parser.add_argument('file', type=FileStorage, location='files', required=False) +# celery 받는거, post&get설정, crud_module에 함수 추가, db_module에 함수 추가 + +@MzRequest.route('', methods=['POST', 'GET']) +@MzRequest.doc(responses={200: 'Success'}) +@MzRequest.doc(responses={404: 'Failed'}) +class MzRequestClass(Resource): + + @MzRequest.expect(post_parser) + def post(self): + """ + # mz 요청 정보 저장 + # @header : token + # @return : {id: "id"} + """ + try: + result, message = crud_module.upload_mz_request() + return Response( + response = json.dumps(message), + status = result, + mimetype = "application/json" + ) + except Exception as ex: + print("******************") + print(ex) + print("******************") + + @MzRequest.expect(common_parser) + def get(self): + """ + # mz 요청 정보 리스트 가져오기 + # @header : token + # @return : {"mz_request_list": result_list} + """ + try: + result, message = crud_module.get_mz_request_list() + return Response( + response = json.dumps(message), + status = result, + mimetype = "application/json" + ) + except Exception as ex: + print("******************") + print(ex) + print("******************") + +@MzRequest.route('/', methods=['GET']) +@MzRequest.doc(responses={200: 'Success'}) +@MzRequest.doc(responses={404: 'Failed'}) +class MzRequestClass(Resource): + + @MzRequest.expect(common_parser) + def get(self, mzRequestId): + """ + # mz 요청 정보 가져오기 + # @header : token + # @return : {"mz_request": result_data} + """ + try: + result, message = crud_module.get_mz_request(mzRequestId) + return Response( + response = json.dumps(message), + status = result, + mimetype = "application/json" + ) + except Exception as ex: + print("******************") + print(ex) + print("******************") + + +@MzRequest.route('//status/') +@MzRequest.doc(responses={200: 'Success'}) +@MzRequest.doc(responses={404: 'Failed'}) +class MzRequestClass(Resource): + + @MzRequest.expect(common_parser) + def get(self, mzRequestId, taskId): + """ + # celery 상태값 가져오기 + # @header : token + # @return : {"mz_request": result_data} + """ + try: + result, message = crud_module.get_celery_task_status(mzRequestId, taskId) + return Response( + response = json.dumps(message), + status = result, + mimetype = "application/json" + ) + except Exception as ex: + print("******************") + print(ex) + print("******************") + +@MzRequest.route('//result/') +@MzRequest.doc(responses={200: 'Success'}) +@MzRequest.doc(responses={404: 'Failed'}) +class MzRequestClass(Resource): + + @MzRequest.expect(common_parser) + def get(self, mzRequestId, taskId): + """ + # celery 결과값 가져오기 + # @header : token + # @return : {"mz_request": result_data} + """ + try: + result, message = crud_module.get_celery_result_done(mzRequestId, taskId) + return Response( + response = json.dumps(message), + status = result, + mimetype = "application/json" + ) + except Exception as ex: + print("******************") + print(ex) + print("******************") + +####################################생성된 결과####################################### + +patch_parser = common_parser.copy() +patch_parser.add_argument('type', location='form', required=False) +patch_parser.add_argument('rating', location='form', required=False) +# parser.add_argument('file', type=FileStorage, location='files', required=False) + +survey_parser = common_parser.copy() +survey_parser.add_argument('user_phone', location='form', required=False) +survey_parser.add_argument('sns_time', location='form', required=False) +survey_parser.add_argument('image_rating_reason', location='form', required=False) +survey_parser.add_argument('voice_to_face_rating', location='form', required=False) +survey_parser.add_argument('dissatisfy_reason', location='form', required=False) +survey_parser.add_argument('additional_function', location='form', required=False) +survey_parser.add_argument('face_to_gif_rating', location='form', required=False) +survey_parser.add_argument('more_gif', location='form', required=False) +survey_parser.add_argument('more_gif_type', location='form', required=False) +survey_parser.add_argument('waiting', location='form', required=False) +survey_parser.add_argument('waiting_improvement', location='form', required=False) +survey_parser.add_argument('recommend', location='form', required=False) +survey_parser.add_argument('opinion', location='form', required=False) + +@MzRequest.route('//mz-result', methods=['POST']) +@MzRequest.doc(responses={200: 'Success'}) +@MzRequest.doc(responses={404: 'Failed'}) +class MzResultClass(Resource): + + @MzRequest.expect(common_parser) + def post(self, mzRequestId): + """ + # mz 결과 재생성 + # @header : token + # @return : {id: "id"} + """ + try: + result, message = crud_module.regenerate_mz_result(mzRequestId) + return Response( + response = json.dumps(message), + status = result, + mimetype = "application/json" + ) + except Exception as ex: + print("******************") + print(ex) + print("******************") + +@MzRequest.route('//mz-result/', methods=['GET', 'POST', 'PATCH']) +@MzRequest.doc(responses={200: 'Success'}) +@MzRequest.doc(responses={404: 'Failed'}) +class MzResultClass(Resource): + + @MzRequest.expect(common_parser) + def get(self, mzRequestId, mzResultId): + """ + # mz 결과 정보 가져오기 + # @header : token + # @return : {"mz_result": result_data} + """ + try: + result, message = crud_module.get_mz_result(mzRequestId, mzResultId) + return Response( + response = json.dumps(message), + status = result, + mimetype = "application/json" + ) + except Exception as ex: + print("******************") + print(ex) + print("******************") + + @MzRequest.expect(survey_parser) + def post(self, mzRequestId, mzResultId): + """ + # mz 설문조사 결과 정보 저장하기 + # @header : token + # @return : {"mz_result_id": "id", "mz_survey_id": "id"} + """ + try: + result, message = crud_module.upload_mz_survey(mzRequestId, mzResultId) + return Response( + response = json.dumps(message), + status = result, + mimetype = "application/json" + ) + except Exception as ex: + print("******************") + print(ex) + print("******************") + + @MzRequest.expect(patch_parser) + def patch(self, mzRequestId, mzResultId): + """ + # mz 결과 별점 수정 + # @header : token + # @return : {"message": "rating updated successfully"} + """ + try: + result, message = crud_module.update_mz_result_rating(mzRequestId, mzResultId) + return Response( + response = json.dumps(message), + status = result, + mimetype = "application/json" + ) + except Exception as ex: + print("******************") + print(ex) + print("******************") diff --git a/backend/apis_v1/User.py b/backend/apis_v1/User.py new file mode 100644 index 0000000..10da86f --- /dev/null +++ b/backend/apis_v1/User.py @@ -0,0 +1,197 @@ +from flask import Response +import json +from module import user_module +from flask_restx import Resource, Namespace + +####################################회원####################################### +Users = Namespace( + name= "Users", + description="Users CRUD를 작성하기 위해 사용하는 API.", +) + +common_parser = Users.parser() + +email_validation_parser = common_parser.copy() +email_validation_parser.add_argument('email', location='form', required=False) + +regist_parser = common_parser.copy() +regist_parser.add_argument('email', location='form', required=False) +regist_parser.add_argument('password', location='form', required=False) +regist_parser.add_argument('age', location='form', required=False, type=int) +regist_parser.add_argument('gender', location='form', required=False) + +token_parser = common_parser.copy() +token_parser.add_argument('token', location='headers') + +@Users.route('/email/validation', methods=['POST']) +@Users.doc(response={200: 'SUCCESS'}) +@Users.doc(response={404: 'Failed'}) +class UserEmailValidaionClass(Resource): + + @Users.expect(email_validation_parser) + def post(self): + """ + # 아이디 중복체크 + # @form-data : email + # @return : 200 or 409 + """ + try: + result, message = user_module.email_validation() + return Response( + response = json.dumps(message), + status = result, + mimetype = "application/json" + ) + except Exception as ex: + print("******************") + print(ex) + print("******************") + +@Users.route('', methods=['POST', 'GET']) +@Users.doc(response={200: 'SUCCESS'}) +@Users.doc(response={404: 'Failed'}) +class UsersClass(Resource): + + @Users.expect(regist_parser) + def post(self): + """ + # 회원가입 + # @form-data : email, password, age, gender + # @return : 200 or 404 + """ + try: + result, message = user_module.create_users() + return Response( + response = json.dumps(message), ##회원가입 성공일때는 status를 201로 받아 나머지는 다 200 + status = result, + mimetype = "application/json" + ) + except Exception as ex: + print("******************") + print(ex) + print("******************") + + @Users.expect(token_parser) + def get(self): + """ + # 회원정보 받아오기 + # @header : token + # @return : {age: "age", gender: "gender"} + """ + try: + result, message = user_module.get_user_info() + return Response( + response = json.dumps(message), + status = result, + mimetype = "application/json" + ) + except Exception as ex: + print("******************") + print(ex) + print("******************") + + +# @Users.route('/email') +# @Users.expect(parser) +# @Users.doc(responses={200: 'Success'}) +# @Users.doc(responses={404: 'Failed'}) +# class UserEmailClass(Resource): + +# def post(self): +# """ +# # 아이디 찾기 +# # @form-data : name, phone +# # @return : {email: "email"} +# """ +# try: +# result, message = user_module.find_email() +# return Response( +# response = json.dumps(message), +# status = result, +# mimetype = "application/json" +# ) +# except Exception as ex: +# print("******************") +# print(ex) +# print("******************") + +# @Users.route('/password/validation') +# @Users.expect(parser) +# @Users.doc(responses={200: 'Success'}) +# @Users.doc(responses={404: 'Failed'}) +# class UserPasswordValidationClass(Resource): + +# def post(self): +# """ +# # 비밀번호 찾기 전 정보 검증 +# # @form-data : email, phone +# # @return : message +# """ +# try: +# result, message = user_module.password_validation() +# return Response( +# response = json.dumps(message), +# status = result, +# mimetype = "application/json" +# ) +# except Exception as ex: +# print("******************") +# print(ex) +# print("******************") + +# @Users.route('/password') +# @Users.expect(parser) +# @Users.doc(responses={200: 'Success'}) +# @Users.doc(responses={404: 'Failed'}) +# class UserUpdatePasswordClass(Resource): + +# def patch(self): +# """ +# # 비밀번호 변경(변경할 비밀번호 정보 받아와서 비밀번호 변경) +# # @form-data : email, phone, password +# # @return : message +# """ +# try: +# result, message = user_module.update_password() +# return Response( +# response = json.dumps(message), +# status = result, +# mimetype = "application/json" +# ) +# except Exception as ex: +# print("******************") +# print(ex) +# print("******************") + +Auth = Namespace( + name= "Auth", + description="User의 Auth를 작성하기 위해 사용하는 API.", +) + +parser = Auth.parser() +parser.add_argument('email', location='form') +parser.add_argument('password', location='form') + +@Auth.route('', methods=['POST']) +@Auth.doc(response={200: 'SUCCESS'}) +@Auth.doc(response={404: 'Failed'}) +class AuthClass(Resource): + + @Auth.expect(parser) + def post(self): + """ + # 로그인 + # @form-data : email, password + # @return : {"token": "token", "user_name": "user_name"} + """ + try: + result, message = user_module.login() + return Response( + response = json.dumps(message), + status = result, + mimetype = "application/json" + ) + except Exception as ex: + print("******************") + print(ex) + print("******************") diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..10b4340 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,58 @@ +from flask import Flask, request, redirect +from flask_cors import CORS +from flask_restx import Api +from db.db_connection import db_connection, db +# from prometheus_flask_exporter import PrometheusMetrics +from apis_v1 import User, MzRequest +import ssl + +app = Flask(__name__) +# app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 용량제한 +app.config.update(DEBUG=True) + +CORS(app, resources={r'*': {'origins': '*'}}, supports_credentials=True) + +db_connection(app) +with app.app_context(): + db.create_all() +# metrics = PrometheusMetrics.for_app_factory() +# metrics.init_app(app) + +api = Api( + app, + version='v1', + title="Make Generator", + description="NAVER boostcamp", + terms_url="/", + contact="vivian0304@naver.com", + license="MIT", + prefix='/api/v1' +) + +api.add_namespace(User.Users, '/users') +api.add_namespace(User.Auth, '/auth') +api.add_namespace(MzRequest.MzRequest, '/mz-request') + +# @app.before_request +# def before_request(): +# if request.url.startswith('http://'): +# url = request.url.replace('http://', 'https://', 1) +# code = 301 +# return redirect(url, code=code) + +# @app.route('/users', methods=['OPTIONS']) +# @app.route('/auth', methods=['OPTIONS']) +# @app.route('/mz-request', methods=['OPTIONS']) +# def preflight(): +# response = flask.Response() +# response.headers['Access-Control-Allow-Origin'] = '*' +# response.headers['Access-Control-Allow-Headers'] = '*' +# response.headers['Access-Control-Allow-Methods'] = '*' + + #return response + +if __name__ == "__main__": +# ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS) +# ssl_context.load_cert_chain(certfile='newcert.pem', keyfile='newkey.pem') +# app.run(port=5050, debug=True,ssl_context=ssl_context) + app.run(port=5050, debug=True) diff --git a/backend/bucket/m_config.py b/backend/bucket/m_config.py new file mode 100644 index 0000000..087a49d --- /dev/null +++ b/backend/bucket/m_config.py @@ -0,0 +1,15 @@ +from dotenv import load_dotenv +import os + +load_dotenv() + +# AWS_ACCESS_KEY = os.getenv('AWS_ACCESS_KEY') +# AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY') +# AWS_S3_BUCKET_REGION = os.getenv('AWS_S3_BUCKET_REGION') +# AWS_S3_BUCKET_NAME = os.getenv('AWS_S3_BUCKET_NAME') +# AWS_S3_BUCKET_URL = os.getenv('AWS_S3_BUCKET_URL') + +ACCESS_KEY = os.getenv("AWS_ACCESS_KEY") +SECRET_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") +BUCKET_NAME = os.getenv("MINIO_BUCKET") +MINIO_API_HOST = os.getenv("MINIO_ENDPOINT") \ No newline at end of file diff --git a/backend/bucket/m_connection.py b/backend/bucket/m_connection.py new file mode 100644 index 0000000..64ab6de --- /dev/null +++ b/backend/bucket/m_connection.py @@ -0,0 +1,62 @@ +from minio import Minio +from bucket.m_config import ACCESS_KEY, SECRET_KEY +from bucket.m_config import BUCKET_NAME, MINIO_API_HOST + +def minio_connection(): + try: + print(MINIO_API_HOST) + print(ACCESS_KEY) + print(SECRET_KEY) + storage = Minio( + MINIO_API_HOST, + ACCESS_KEY, + SECRET_KEY, + secure=True, + ) + except Exception as e: + print(e) + return False + else: + print("storage bucket connected!") + return storage + +def minio_put_object(storage, filename, data): + ''' + minio bucket에 지정 파일 업로드 + :param minio: 연결된 minio 객체(Minio client) + :param filename: 파일 위치 + :param data: 데이터 + :return: 성공 시 True, 실패 시 False 반환 + ''' + try: + storage.fput_object(BUCKET_NAME, filename, data, content_type="audio/wav") + #storage.fput_object(BUCKET_NAME, filename, data) + print(f"{filename} is successfully uploaded to bucket {BUCKET_NAME}.") + except Exception as e: + print(e) + return False + return True + +# def minio_list_object(storage, age, gender): +# ''' +# minio bucket에서 해당 성별과 나이에 맞는 이미지 리스트 가져오기 +# :param storage: 연결된 minio 객체(Minio client) +# :param prefix: Object name starts with prefix. +# :param age: 나이 +# :param gender: 성별 +# :return: 성공 시 list 반환, 실패 시 False 반환 +# ''' +# try: +# prefix = "output_condition" +# contents_list = storage.list_objects(bucket, prefix)['Contents'] +# file_list = [content['Key'] for content in contents_list] +# condition_file_list = [] +# for file in file_list: +# _, file_name = file.split('-') +# idx = file_name.rindex('.') +# if file_name[idx+1:] == 'jpg' and file_name[:idx] == f'{gender}_{age}': +# condition_file_list.append(file) +# except Exception as e: +# print(e) +# return False +# return condition_file_list \ No newline at end of file diff --git a/backend/db/db_config.py b/backend/db/db_config.py new file mode 100644 index 0000000..eae4ab4 --- /dev/null +++ b/backend/db/db_config.py @@ -0,0 +1,12 @@ +# HOST = 'db' #docker db +# PORT = 27017 #defalut port + +from dotenv import load_dotenv +import os + +load_dotenv() + +USERNAME = os.getenv('USERNAME') +PASSWORD = os.getenv('PASSWORD') +HOST = os.getenv('HOST') +DATABASE = os.getenv('DATABASE') diff --git a/backend/db/db_connection.py b/backend/db/db_connection.py new file mode 100644 index 0000000..6181a2d --- /dev/null +++ b/backend/db/db_connection.py @@ -0,0 +1,16 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from db.db_config import USERNAME, PASSWORD, HOST, DATABASE + +db = SQLAlchemy() + +def db_connection(app): + + app.config['SQLALCHEMY_DATABASE_URI'] = f'mysql+pymysql://{USERNAME}:{PASSWORD}@{HOST}/{DATABASE}' + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + + db.init_app(app) + migrate = Migrate(app, db) + print(db) + diff --git a/backend/db/enum_classes.py b/backend/db/enum_classes.py new file mode 100644 index 0000000..f9c5b9f --- /dev/null +++ b/backend/db/enum_classes.py @@ -0,0 +1,23 @@ +import enum + +class ScopeClass(enum.Enum): + user = "user" + origin = "origin" + +class StatusClass(enum.Enum): + success = "SUCCESS" + origin = "ORIGIN" + processed = "PROCCESSED" + deleted = "DELETED" + failure = "FAILURE" + pending = "PENDING" + +class FaceTypeClass(enum.Enum): + mosaic = "mosaic" + character = "character" + +class SchemaName(enum.Enum): + user = "user" + mzRequest = "mzRequest" + mzResult = "mzResult" + video = "video" diff --git a/backend/db/schema.py b/backend/db/schema.py new file mode 100644 index 0000000..f28ce5c --- /dev/null +++ b/backend/db/schema.py @@ -0,0 +1,127 @@ +from db.db_connection import db +from datetime import datetime +from db.enum_classes import ScopeClass, StatusClass, FaceTypeClass + +class User(db.Model): + __tablename__ = 'users' + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(50), unique=True, nullable=False) + password = db.Column(db.String(255), nullable=False) + age = db.Column(db.Integer, nullable=True) + gender = db.Column(db.String(10), nullable=True) + created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) + deleted_at = db.Column(db.DateTime, onupdate=datetime.utcnow) + + def get_email(self): + return self.email + + def get_id(self): + # In SQLAlchemy, primary key field is automatically named `id` + return self.id + + def __init__(self, email, password, age, gender, created_at): + self.email = email + self.password = password + self.age = age + self.gender = gender + self.created_at = created_at + +class MzRequest(db.Model): + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) + age = db.Column(db.Integer, nullable=True) + gender = db.Column(db.String(10), nullable=True) + voice_url = db.Column(db.String(255), nullable=True) + status = db.Column(db.String(10), nullable=True, default='Proceeding') + ata = db.Column(db.TIMESTAMP, nullable=True) # 완료시간 + # celery_id = + # celery_id = db.Column(db.String(255), nullable=True) + created_at = db.Column(db.TIMESTAMP, nullable=True) + updated_at = db.Column(db.TIMESTAMP, nullable=True) + deleted_at = db.Column(db.TIMESTAMP, nullable=True) + + def __init__(self, user_id, age, gender, voice_url, status, ata, created_at): + self.user_id = user_id + self.age = age + self.gender = gender + self.voice_url = voice_url + self.status = status + self.ata = ata + self.created_at = created_at + +class MzResult(db.Model): + id = db.Column(db.Integer, primary_key=True) + mz_request_id = db.Column(db.Integer, db.ForeignKey('mz_request.id'), nullable=False) + condition_image_url = db.Column(db.String(255), nullable=True) + condition_gif_url = db.Column(db.String(255), nullable=True) + voice_image_url = db.Column(db.String(255), nullable=True) + voice_gif_url = db.Column(db.String(255), nullable=True) + condition_image_rating = db.Column(db.Integer, nullable=True) + #condition_gif_rating = db.Column(db.Integer, nullable=True) + voice_image_rating = db.Column(db.Integer, nullable=True) + #voice_gif_rating = db.Column(db.Integer, nullable=True) + condition_image_score = db.Column(db.Float, nullable=True) + condition_gif_score = db.Column(db.Float, nullable=True) + voice_image_score = db.Column(db.Float, nullable=True) + voice_gif_score = db.Column(db.Float, nullable=True) + survey = db.Column(db.Integer, default=0) + created_at = db.Column(db.TIMESTAMP, nullable=False) + updated_at = db.Column(db.TIMESTAMP, nullable=True) + deleted_at = db.Column(db.TIMESTAMP, nullable=True) + + def __init__(self, mz_request_id, created_at): + self.mz_request_id = mz_request_id + self.created_at = created_at + +class MzSurvey(db.Model): + id = db.Column(db.Integer, primary_key=True) + mz_result_id = db.Column(db.Integer, db.ForeignKey('mz_result.id'), nullable=False) + user_phone = db.Column(db.String(50), nullable=True) + sns_time = db.Column(db.Integer, nullable=True) + image_rating_reason = db.Column(db.Text, nullable=True) + voice_to_face_rating = db.Column(db.Integer, nullable=True) + dissatisfy_reason = db.Column(db.String(10), nullable=True) # 복수 선택 + additional_function = db.Column(db.Text, nullable=True) + face_to_gif_rating = db.Column(db.Integer, nullable=True) + more_gif = db.Column(db.Integer, nullable=True) + more_gif_type = db.Column(db.String(10), nullable=True) # 복수 선택 + waiting = db.Column(db.Integer, nullable=True) + waiting_improvement = db.Column(db.Integer, nullable=True) + recommend = db.Column(db.Integer, nullable=True) + opinion = db.Column(db.Text, nullable=True) + created_at = db.Column(db.TIMESTAMP, nullable=True) + updated_at = db.Column(db.TIMESTAMP, nullable=True) + deleted_at = db.Column(db.TIMESTAMP, nullable=True) + + def __init__(self, + mz_result_id, + user_phone, + sns_time, + image_rating_reason, + voice_to_face_rating, + dissatisfy_reason, + additional_function, + face_to_gif_rating, + more_gif, + more_gif_type, + waiting, + waiting_improvement, + recommend, + opinion, + created_at): + self.mz_result_id = mz_result_id + self.user_phone = user_phone + self.sns_time = sns_time + self.image_rating_reason = image_rating_reason + self.voice_to_face_rating = voice_to_face_rating + self.dissatisfy_reason = dissatisfy_reason + self.additional_function = additional_function + self.face_to_gif_rating = face_to_gif_rating + self.more_gif = more_gif + self.more_gif_type = more_gif_type + self.waiting = waiting + self.waiting_improvement = waiting_improvement + self.recommend = recommend + self.opinion = opinion + self.created_at = created_at \ No newline at end of file diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..de59308 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,77 @@ +version: '3' +services: + frontend: + container_name: frontend + image: "makezenerator/frontend:latest" + environment: + NODE_ENV: production + ports: + - 3000:3000 + command: pm2-runtime start ./build/index.js --env production + + backend: + restart: unless-stopped + container_name: backend + image: "makezenerator/backend:latest" + ports: + - 5050:5050 + env_file: + .env.prod + environment: + FLASK_DEBUG: 1 #리로딩 설정 + command: gunicorn -w 1 -b 0.0.0.0:5050 app:app --reload + + rabbitmq: + container_name: rabbitmq + hostname: rabbit + image: "rabbitmq:3-management" + environment: + - RABBITMQ_DEFAULT_USER=admin + - RABBITMQ_DEFAULT_PASS=mypass + ports: + - "15672:15672" + - "5672:5672" + + simple_worker: + container_name: simple_worker + image: "makezenerator/simple_worker" + env_file: + .env.prod + depends_on: + - rabbitmq + - backend + + nginx: + container_name: nginx + image: nginx:latest + restart: unless-stopped + volumes: + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + - ./conf/nginx.conf:/etc/nginx/nginx.conf + - ./conf/makezenerator.com.conf:/etc/nginx/conf.d/makezenerator.com.conf + - ./conf/api.makezenerator.com.conf:/etc/nginx/conf.d/api.makezenerator.com.conf + ports: + - 80:80 + - 443:443 + command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" + + certbot: + container_name: certbot + image: certbot/certbot + restart: unless-stopped + volumes: + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + + node: + image: prom/node-exporter + container_name: node-exporter + ports: + - 9100:9100 + networks: + - promnet +networks: + promnet: + driver: bridge \ No newline at end of file diff --git a/backend/module/crud_module.py b/backend/module/crud_module.py new file mode 100644 index 0000000..da3e2cc --- /dev/null +++ b/backend/module/crud_module.py @@ -0,0 +1,499 @@ +from flask import request +from db.enum_classes import ScopeClass, StatusClass, FaceTypeClass, SchemaName +import module +from static import status_code +from module import db_module, file_module +from celery import Celery +from db.db_config import USERNAME, PASSWORD, HOST, DATABASE + +# Create a Celery instance named 'celery_app' +celery = Celery('tasks', broker='amqp://admin:mypass@rabbit:5672') + +# Configure Celery settings +celery.conf.update( + # Configure the result backend using MySQL + CELERY_RESULT_BACKEND=f'db+mysql+pymysql://{USERNAME}:{PASSWORD}@{HOST}/{DATABASE}', + + # Set the task serializer to JSON + CELERY_TASK_SERIALIZER='json', + + # Do not ignore results (set to False) + CELERY_IGNORE_RESULT=False, +) +REQUEST_LIMIT = 10 +################### MZ REQUEST ################### +""" +* TODO mz request upload +""" +def upload_mz_request(): + try: + token = request.headers.get('Token') + user = module.token.get_user(token) + if user == False: + return 401, {"error": status_code.token_error} + # check the number of requests + result, message = module.db_module.count_mz_request_list(user) + print(result, message) + if result == 200: + request_count = message['request_count'] + if request_count >= REQUEST_LIMIT: + return 405, {"error": request_count} + else: + return result, message + + age = request.form.get('age') + if age == None or not age.isdigit(): + return 404, {"error": f'{status_code.field_error}age'} + gender = request.form.get('gender') + if gender == None or (gender != 'man' and gender != 'woman'): + return 404, {"error": f'{status_code.field_error}gender'} + voice_url = None + status = request.form.get('status') + ata = request.form.get('ata') + + result, message = module.db_module.create_mz_request(user, age, gender, voice_url, status, ata) + request_id = message['mz_request_id'] + result_id = message['mz_result_id'] + + if 'file' not in request.files: + return 404, {"error": f'{status_code.field_error}file'} + filename = request.files['file'].filename + if filename == None or filename == '': + return 404, {"error": f'{status_code.field_error}file'} + f = request.files['file'] + file_result, location = file_module.file_upload(request_id, result_id, SchemaName.mzRequest.value, f) + if file_result == False: + return 400, location + + _, _ = module.db_module.update_mz_request_voice_url(request_id, location) + + celery_task_id = celery.send_task('tasks.run_mz', kwargs= + { + 'request_id' : request_id, + 'result_id' : result_id, + 'age' : age, + 'gender' : gender, + 'file_url' : location + }) + + print("==========================") + print(celery_task_id) + print("==========================") + message["celery_task_id"] = str(celery_task_id) + + return result, message + except Exception as ex: + print(ex) + return 400, {"error": str(ex)} + +""" +* mz request get +""" +def get_mz_request(mz_request_id): + try: + token = request.headers.get("Token") + user_id = module.token.get_user(token) + if user_id == False: + return 401, {"error": status_code.token_error} + result, message = module.db_module.read_mz_request(mz_request_id, user_id) + return result, message + except Exception as ex: + print(ex) + return 400, {"error": str(ex)} + +""" +* mz request list get +""" +def get_mz_request_list(): + try: + token = request.headers.get("Token") + user_id = module.token.get_user(token) + if user_id == False: + return 401, {"error": status_code.token_error} + result, message = module.db_module.read_mz_request_list(user_id) + return result, message + except Exception as ex: + print(ex) + return 400, {"error": str(ex)} + +################### MZ RESULT ################### +""" +* TODO mz result regenerate +""" +def regenerate_mz_result(mz_request_id): + try: + token = request.headers.get("Token") + user_id = module.token.get_user(token) + if user_id == False: + return 401, {"error": status_code.token_error} + # 다시 생성중으로 변경 + result, message = module.db_module.update_mz_request_status(mz_request_id, 'Proceeding') + if result != 200: + return result, message + + result, mz_result_id = module.db_module.create_mz_result(mz_request_id) + _, request_info = module.db_module.read_mz_request(mz_request_id, user_id) + if _ !=200: + return 400, {'error' : request_info['error']} + info = request_info['mz_request'] + celery_task_id = celery.send_task('tasks.run_mz', kwargs= + { + 'request_id' : mz_request_id, + 'result_id' : mz_result_id, + 'age' : info['age'], + 'gender' : info['gender'], + 'file_url' : info['voice_url'] + }) + + print("==========================") + print(celery_task_id) + print("==========================") + + return result, {"mz_request_id" : str(mz_request_id), "regenerate_mz_result_id" : str(mz_result_id)} + except Exception as ex: + print(ex) + return 400, {"error": str(ex)} + +""" +* mz result get +""" +def get_mz_result(mz_request_id, mz_result_id): + try: + token = request.headers.get("Token") + user_id = module.token.get_user(token) + if user_id == False: + return 401, {"error": status_code.token_error} + result, message = module.db_module.read_mz_result(mz_request_id, mz_result_id) + return result, message + except Exception as ex: + print(ex) + return 400, {"error": str(ex)} + +""" +* mz result rating update +""" +def update_mz_result_rating(mz_request_id, mz_result_id): + try: + token = request.headers.get("Token") + user_id = module.token.get_user(token) + if user_id == False: + return 401, {"error": status_code.token_error} + + rating_type = request.form.get('type') + if rating_type != 'voice' and rating_type != 'condition': + return 404, {"error": f'{status_code.field_error}rating_type'} + rating_num = request.form.get('rating') + if rating_num == None or not rating_num.isdigit(): + return 404, {"error": f'{status_code.field_error}rating_num'} + + result, message = module.db_module.update_mz_result_rating(mz_request_id, mz_result_id, rating_type, rating_num) + return result, message + except Exception as ex: + print(ex) + return 400, {"error": str(ex)} + +################### MZ SURVEY ################### +def upload_mz_survey(mz_request_id, mz_result_id): + try: + token = request.headers.get('Token') + user = module.token.get_user(token) + if user == False: + return 401, {"error": status_code.token_error} + + user_phone = request.form.get('user_phone') + sns_time = request.form.get('sns_time') + image_rating_reason = request.form.get('image_rating_reason') + voice_to_face_rating = request.form.get('voice_to_face_rating') + dissatisfy_reason = request.form.get('dissatisfy_reason') + additional_function = request.form.get('additional_function') + face_to_gif_rating = request.form.get('face_to_gif_rating') + more_gif = request.form.get('more_gif') + more_gif_type = request.form.get('more_gif_type') + waiting = request.form.get('waiting') + waiting_improvement = request.form.get('waiting_improvement') + recommend = request.form.get('recommend') + opinion = request.form.get('opinion') + + + result, message = module.db_module.create_mz_survey(mz_result_id, + user_phone, + sns_time, + image_rating_reason, + voice_to_face_rating, + dissatisfy_reason, + additional_function, + face_to_gif_rating, + more_gif, + more_gif_type, + waiting, + waiting_improvement, + recommend, + opinion) + _, _ = module.db_module.update_mz_result_survey(mz_request_id, mz_result_id, 1) + + return result, message + except Exception as ex: + print(ex) + return 400, {"error": str(ex)} + +# 1. 백엔드에 샐러리 ID가 있다면 샐러리 ID와 요청해야하는 request_id를 함께 넘겨주기 +# 2. 백엔드에서는 result ID로 조회 +# 1) 만약 DB 존재한다면 -> DB에서 찾은 정보 바로 return +# 2) 존재하지 않는다면 -> status 조회 후 SUCCESS가 나오는 경우, result_db에 저장하고 return + + +# """ +# * celery status +# * Define a route for getting the status of a task +# """ +# def get_celery_task_status(mz_request_id, task_id): +# try: +# token = request.headers.get("Token") +# user_id = module.token.get_user(token) +# if user_id == False: +# return 401, {"error": status_code.token_error} +# result, message = module.db_module.read_mz_request(mz_request_id, user_id) +# if result == 200: +# # Get the status of the task using the task ID +# task_status = celery.AsyncResult(task_id).status +# # Update status in DB +# try: +# result, message = module.db_module.update_mz_request_status(mz_request_id, task_status) +# except Exception as ex: +# print(ex) +# return 400, {"error": str(ex)} + +# # Return the task status as a JSON response +# return 200, {'Task Status': task_status} +# else: +# return 400, {'Cannot find mz_request'} +# except Exception as ex: +# print(ex) +# return 400, {"error": str(ex)} + +# """ +# * celery result +# * Define a route for getting the result of a completed task +# """ +# def get_celery_result_done(mz_request_id, task_id): +# try: +# token = request.headers.get("Token") +# user_id = module.token.get_user(token) +# if user_id == False: +# return 401, {"error": status_code.token_error} +# result, message = module.db_module.read_mz_request(mz_request_id, user_id) +# if result == 200: +# # Get the result of the task using the task ID +# task_result = celery.AsyncResult(task_id).result + +# # Update image and gif in result record +# try: +# result, message = module.db_module.update_mz_result_image_gif(task_result) +# except Exception as ex: +# print(ex) +# return 400, {"error": str(ex)} + +# # Return the task result as a JSON response +# return 200, {'Task Result': task_result} +# else: +# return 400, {'Cannot find mz_request'} +# except Exception as ex: +# print(ex) +# return 400, {"error": str(ex)} + +# """ +# * Whitelist face image delete +# """ +# def delete_whitelist_face_image(whitelistFaceId, _id): +# try: +# token = request.headers.get('Token') +# user = module.token.get_user(token) +# if user == False: +# return 401, {"error": status_code.token_error} +# result, message = module.db_module.delete_whitelist_face_image(whitelistFaceId, _id) +# return result, message +# except Exception as ex: +# print(ex) +# return 400, {"error": str(ex)} + +# ################### VIDEO ################### + +# """ +# * origin video upload +# """ +# def origin_video_upload(): +# try: +# token = request.headers.get('Token') +# user = module.token.get_user(token) +# if user == False: +# return 401, {"error": status_code.token_error} +# if 'file' not in request.files: +# return 404, {"error": f'{status_code.field_error}file'} +# filename = request.files['file'].filename +# if filename == None or filename == '': +# return 404, {"error": f'{status_code.field_error}file'} +# f = request.files['file'] +# fileResult, location = file_module.file_upload(user, SchemaName.video.value, f) +# if fileResult == False: +# return fileResult, location +# result, message = module.db_module.create_video(user, location) +# if result == True: ###### result-> result == 200 +# return 200, {"id" : message, "url": location} +# else: +# return 400, message +# except Exception as ex: +# print(ex) +# return 400, {"error": str(ex)} + +# """ +# * video delete +# """ +# def delete_video(_id): +# try: +# token = request.headers.get('Token') +# user = module.token.get_user(token) +# if user == False: +# return False, {"error": status_code.token_error} +# result, message = db_module.delete_video(user, _id) +# return result, message +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} + +# """ +# * update video before save s3 +# """ +# def update_video_upload(): +# try: +# token = request.headers.get('Token') +# user = module.token.get_user(token) +# if user == False: +# return False, {"error": status_code.token_error} + +# # video id가져옴 +# videoId = request.form.get('video_id') +# if videoId == None or videoId == '': +# return False, {"error": f'{status_code.field_error}video_id'} + +# # video url 찾기 +# result, videoUrl = db_module.read_origin_video(videoId, user) +# if result == False: +# return result, videoUrl + +# # faceType 가져옴 +# faceType = request.form.get('face_type') +# if faceType == None or faceType == '': +# return False, {"error": f'{status_code.field_error}face_type'} + +# if faceType != FaceTypeClass.character.value and faceType != FaceTypeClass.mosaic.value: +# return False, {"error": f'{status_code.enum_class_error}face_type'} + +# # blockCharacterId 선택적으로 가져옴 +# if faceType == FaceTypeClass.character.value: +# blockCharacterId = request.form.get('block_character_id') +# if blockCharacterId == None or blockCharacterId == '': +# return False, {"error": f'{status_code.field_error}block_character_id'} +# result, blockCharacterImg = db_module.read_block_character_url(blockCharacterId) +# if result == False: +# return result, blockCharacterImg +# else: +# blockCharacterId = None +# blockCharacterImg = None + +# # TODO - whitelistFaceId 없을 경우에 대한 것 처리하기 +# whitelistFaceId = request.form.getlist("whitelist_face_id") +# result, whitelistFaceImgList = db_module.read_whitelist_face_url(user, whitelistFaceId) +# if result == False: +# return result, whitelistFaceImgList + +# # True or False 리턴 +# result, message = db_module.update_video(videoId, user, faceType, whitelistFaceId, blockCharacterId) # ID를 받아와서 찾은다음에 url + +# if result == True: +# if faceType == FaceTypeClass.mosaic.value: +# task = celery.send_task('tasks.run_mosaic', kwargs= +# { +# 'whitelistFaceImgList' : whitelistFaceImgList, # url 리스트 +# 'videoUrl' : videoUrl, +# "user" : str(user) +# }) +# result2, message = db_module.update_video_celery(videoId, user, task, task.status) + +# if result2 == True: +# return True, {"id" : str(task.id)} +# else: +# return False, {"error":status_code.update_02_fail} +# elif faceType == FaceTypeClass.character.value: +# task = celery.send_task('tasks.run_character', kwargs= +# { +# 'whitelistFaceImgList' : whitelistFaceImgList, +# 'blockCharacterImgUrl' : blockCharacterImg, +# 'videoUrl' : videoUrl, +# "user" : str(user) +# }) +# result2, message = db_module.update_video_celery(videoId, user, task, task.status) + +# if result2 == True: +# return True, {"id" : str(task.id)} +# else: +# return False, {"error":status_code.update_02_fail} +# else: +# return result, message +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} + +# """ +# * 셀러리 id를 통해 상태 체크 +# """ +# def get_after_video_status(taskId): +# try: +# token = request.headers.get("Token") +# user = module.token.get_user(token) +# if user == False: +# return False, {"error": status_code.token_error} +# result, message = db_module.read_celery_status(user, taskId) +# if message == StatusClass.failure.value: +# status = celery.AsyncResult(taskId, app=celery) +# result2 = db_module.update_video_celery_failure(user, taskId) #video컬렉션의 status를 FAILURE로 업데이트 +# if result2 == True: +# return True, {"status" : StatusClass.failure.value} #셀러리의 결과과 failure이고, video 컬렉션의 status 업데이트를 성공한 경우 +# else: +# return False, {"error", status_code.update_02_fail} #셀러리의 결과과 failure이고, video 컬렉션의 status 업데이트를 실패한 경우 +# elif result == 0: +# return True, {"status" : StatusClass.pending.value} #PENDING +# else: +# return True, {"status" : message} #SUCCESS +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} + +# """ +# * 특정 유저에 대한 비디오 결과 모두 조회하기 +# """ +# def get_multiple_after_video(): +# try: +# token = request.headers.get("Token") +# user = module.token.get_user(token) +# if user == False: +# return False, {"error": status_code.token_error} +# result, message = module.db_module.read_proccessed_video(user) +# return result, message +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} + +# """ +# * read celery task status +# """ +# def read_celery_task_status(taskId): +# try: +# status = celery.AsyncResult(taskId, app=celery) +# if status == StatusClass.success: +# result = celery.AsyncResult(taskId).result +# return True, {"status", result} +# else: +# return False, {"status", status} +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} \ No newline at end of file diff --git a/backend/module/db_module.py b/backend/module/db_module.py new file mode 100644 index 0000000..b04352d --- /dev/null +++ b/backend/module/db_module.py @@ -0,0 +1,570 @@ +from datetime import datetime +from db.enum_classes import ScopeClass, StatusClass, FaceTypeClass +from static import status_code +from db import schema +from db.db_connection import db +from sqlalchemy import desc +from pytz import timezone + +################### MZ REQUEST ################### +""" +* mz request create +""" +def create_mz_request(user, age, gender, voice_url, status, ata): + try: + new_mz_request = schema.MzRequest(user_id=user, + age=age, + gender=gender, + voice_url=voice_url, + status=status, + ata=ata, + created_at=datetime.now(timezone('Asia/Seoul'))) + db.session.add(new_mz_request) + db.session.commit() + if new_mz_request.id is not None: + result, mz_result_id = create_mz_result(new_mz_request.id) + return 200, {"mz_request_id" : str(new_mz_request.id), "mz_result_id" : str(mz_result_id)} #true->200 + except Exception as ex: + db.session.rollback() + print(ex) + return 400, {"error": str(ex)} #false->400 + +""" +* mz request read +""" +def read_mz_request(id, user_id): + try: + mz_request = schema.MzRequest.query.filter_by(id = id, user_id = user_id, deleted_at=None).first() + if mz_request: + result_data = { + "id": mz_request.id, + "age": mz_request.age, + "gender": mz_request.gender, + "voice_url": mz_request.voice_url, + "status": mz_request.status, + "ata": mz_request.ata.isoformat() if mz_request.ata else None, + "created_at": mz_request.created_at.isoformat(), + "updated_at": mz_request.updated_at.isoformat() if mz_request.updated_at else None + } + return 200, {"mz_request": result_data} #true->200 + else: + return 404, {"error": "MZ request not found"} + except Exception as ex: + print(ex) + return 400, {"error": str(ex)} #false->400 +""" +* count mz request which is not failed +""" +def count_mz_request_list(user_id): + try: + mz_request_list = schema.MzRequest.query.filter(schema.MzRequest.user_id==user_id, + schema.MzRequest.deleted_at==None, + schema.MzRequest.status!='Failed').all() + if mz_request_list == None: + request_count = 0 + else: + request_count = len(mz_request_list) + return 200, {'request_count' : request_count} + except Exception as ex: + print(ex) + return 400, {"error": str(ex)} #false->400 +""" +* mz request list read +""" +def read_mz_request_list(user_id): + try: + mz_request_list = schema.MzRequest.query.filter_by(user_id=user_id, deleted_at=None).order_by(desc(schema.MzRequest.id)).all() + + # 결과 리스트를 준비합니다. + result_list = [] + + for mz_request in mz_request_list: + # 각 MzRequest에 대한 MzResult 중 가장 최신의 결과 찾기 + latest_mz_result = schema.MzResult.query.filter_by(mz_request_id=mz_request.id).order_by(desc(schema.MzResult.id)).first() + + # MzRequest 정보와 함께 최신 MzResult의 id를 결과 리스트에 추가 + result_data = { + "id": mz_request.id, + "user_id": mz_request.user_id, + "age": mz_request.age, + "gender": mz_request.gender, + "voice_url": mz_request.voice_url, + "status": mz_request.status, + "ata": mz_request.ata.isoformat() if mz_request.ata else None, + "latest_mz_result_id": latest_mz_result.id if latest_mz_result else None, # 최신 MzResult의 ID 추가 + "created_at": mz_request.created_at.isoformat() if mz_request.created_at else None, + "updated_at": mz_request.updated_at.isoformat() if mz_request.updated_at else None, + } + result_list.append(result_data) + return 200, {"mz_request_list": result_list} + except Exception as ex: + print(ex) + return 400, {"error": str(ex)} #false->400 + +""" +* mz request voice_url update +""" +def update_mz_request_voice_url(mz_request_id, location): + try: + mz_request = schema.MzRequest.query.filter_by(id = mz_request_id).first() + if mz_request: + mz_request.voice_url = location + # mz_request.updated_at = datetime.now(timezone('Asia/Seoul')) + db.session.commit() + return 200, {"message": "status updated sucessfully"} + else: + return 404, {"error": str("Can't find mz request")} + except Exception as ex: + db.session.rollback() + print(ex) + return 400, {"error": str(ex)} #false->400 + + +""" +* mz request status update +""" +def update_mz_request_status(mz_request_id, task_status): + try: + mz_request = schema.MzRequest.query.filter_by(id = mz_request_id).first() + if mz_request: + mz_request.status = task_status + if task_status == 'Success': + mz_request.updated_at = datetime.now(timezone('Asia/Seoul')) + db.session.commit() + return 200, {"message": "status updated sucessfully"} + else: + return 404, {"error": str("Can't find mz request")} + except Exception as ex: + db.session.rollback() + print(ex) + return 400, {"error": str(ex)} #false->400 + + +################### MZ RESULT ################### +""" +* mz result create +""" +def create_mz_result(mz_request_id): + try: + new_mz_result = schema.MzResult(mz_request_id, datetime.now(timezone('Asia/Seoul'))) + db.session.add(new_mz_result) + db.session.commit() + return 200, new_mz_result.id #true->200 + except Exception as ex: + db.session.rollback() + print(ex) + return 400, {"error": str(ex)} #false->400 + +""" +* mz result read +""" +def read_mz_result(mz_request_id, mz_result_id): + try: + mz_result = schema.MzResult.query.filter_by(mz_request_id = mz_request_id, id = mz_result_id, deleted_at=None).first() + if mz_result: + result_data = { + "id": mz_result.id, + "mz_request_id": mz_result.mz_request_id, + "condition_image_url": mz_result.condition_image_url, + "condition_gif_url": mz_result.condition_gif_url, + "voice_image_url": mz_result.voice_image_url, + "voice_gif_url": mz_result.voice_gif_url, + "condition_image_rating": mz_result.condition_image_rating, + "voice_image_rating": mz_result.voice_image_rating, + "created_at": mz_result.created_at.isoformat(), + "updated_at": mz_result.updated_at.isoformat() if mz_result.updated_at else None, + "survey" : mz_result.survey, + } + return 200, {"mz_result": result_data} #true->200 + else: + return 404, {"error": "MZ result not found"} + except Exception as ex: + print(ex) + return 400, {"error": str(ex)} #false->400 + +# """ +# * mz result image/gif update +# """ +# def update_mz_result_image_gif(mz_request_id, task_result): +# try: +# mz_result = schema.MzResult.query.filter_by(mz_request_id = mz_request_id, id = task_result.result_id) +# if mz_result: +# mz_result.condition_image_url = task_result.condition_image_url +# mz_result.condition_gif_url = task_result.condition_gif_url +# mz_result.voice_image_url = task_result.voice_image_url +# mz_result.voice_gif_url = task_result.voice_gif_url +# mz_result.updated_at = datetime.now(timezone('Asia/Seoul')) +# db.session.commit() +# return 200, {"message": "image and gif updated sucessfully"} +# else: +# return 404, {"error": str("Can't find mz result")} +# except Exception as ex: +# db.session.rollback() +# print(ex) +# return 400, {"error": str(ex)} #false->400 +""" +* mz result rating update +""" +def update_mz_result_rating(mz_request_id, mz_result_id, rating_type, rating_num): + try: + mz_result = schema.MzResult.query.filter_by(mz_request_id = mz_request_id, id=mz_result_id, deleted_at=None).first() + if mz_result: + if rating_type == 'voice': + mz_result.voice_image_rating = rating_num + elif rating_type == 'condition': + mz_result.condition_image_rating = rating_num + mz_result.updated_at = datetime.now(timezone('Asia/Seoul')) + db.session.commit() + return 200, {"message": "rating updated successfully"} #true->200 + else: + return 404, {"error": str("Can't find mz result")} + except Exception as ex: + db.session.rollback() + print(ex) + return 400, {"error": str(ex)} #false->400 +""" +* mz result survey update +""" +def update_mz_result_survey(mz_request_id, mz_result_id, survey): + try: + mz_result = schema.MzResult.query.filter_by(mz_request_id = mz_request_id, id=mz_result_id, deleted_at=None).first() + if mz_result: + mz_result.survey = survey + mz_result.updated_at = datetime.now(timezone('Asia/Seoul')) + db.session.commit() + return 200, {"message": "rating updated successfully"} #true->200 + else: + return 404, {"error": str("Can't find mz result")} + except Exception as ex: + db.session.rollback() + print(ex) + return 400, {"error": str(ex)} #false->400 + +################### MZ SURVEY ################### +def create_mz_survey(result_id, + user_phone, + sns_time, + image_rating_reason, + voice_to_face_rating, + dissatisfy_reason, + additional_function, + face_to_gif_rating, + more_gif, + more_gif_type, + waiting, + waiting_improvement, + recommend, + opinion): + try: + new_mz_survey = schema.MzSurvey(mz_result_id=result_id, + user_phone=user_phone, + sns_time=sns_time, + image_rating_reason=image_rating_reason, + voice_to_face_rating=voice_to_face_rating, + dissatisfy_reason=dissatisfy_reason, + additional_function=additional_function, + face_to_gif_rating=face_to_gif_rating, + more_gif=more_gif, + more_gif_type=more_gif_type, + waiting=waiting, + waiting_improvement=waiting_improvement, + recommend=recommend, + opinion=opinion, + created_at=datetime.now(timezone('Asia/Seoul'))) + db.session.add(new_mz_survey) + db.session.commit() + return 200, {"mz_result_id" : str(result_id), "mz_survey_id" : str(new_mz_survey.id)} #true->200 + except Exception as ex: + db.session.rollback() + print(ex) + return 400, {"error": str(ex)} #false->400 +# """ +# * WhitelistFaceImage delete +# """ +# def delete_whitelist_face_image(whitelistFaceId, _id): +# try: +# whilelistFaceImage = schema.WhitelistFaceImage.objects(_id = ObjectId(_id), whitelist_face_id = whitelistFaceId, is_deleted=False) +# if whilelistFaceImage.count() == 0: +# return 404, {"error": f'{status_code.id_error}whitelist_face_image'} #false->404 +# deleteWhilelistFaceImage = whilelistFaceImage.update( +# is_deleted=True, +# updated_at =datetime.now() +# ) +# if deleteWhilelistFaceImage > 0: +# return 200, {"message": status_code.delete_01_success} #true->200 +# else: +# print("Can't be deleted") +# return 404, {"error": status_code.delete_02_fail} #false->404 +# except Exception as ex: +# print(ex) +# return 400, {"error": str(ex)} #false->400 + +# """ +# * WhitelistFaceImg corresponding to id read +# """ +# def read_whitelist_face_url(user, whitelistFaceId): #id 리스트로 받아옴 +# try: +# whitelistFaceImgList = [] +# for x in whitelistFaceId: +# temp = schema.WhitelistFace.objects(user_id = ObjectId(user), _id = ObjectId(x), is_deleted = False).first() +# for y in schema.WhitelistFaceImage.objects(whitelist_face_id = temp._id, is_deleted=False): +# whitelistFaceImgList.append(y.url) +# return True, whitelistFaceImgList +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} + +# ################################################ BLOCkCHARACTER ################################################ +# """ +# * OriginBlockCharacter create +# """ +# def create_origin_block_character(location): +# try: +# blockCharacter = schema.BlockCharacter(location, ScopeClass.origin.value, False, datetime.now()) +# result = schema.BlockCharacter.objects().insert(blockCharacter) +# return 200, {"id": str(result._id)} #true->200 +# except Exception as ex: +# print(ex) +# return 400, {"error": str(ex)} #false->400 + +# """ +# * OriginBlockCharacter read +# """ +# def read_origin_block_character(): +# try: +# temp = schema.BlockCharacter.objects(scope = ScopeClass.origin.value, is_deleted=False) +# tempJson = {} +# tempJson["data"] = [] +# for x in temp: +# tempJson1 = {"id" : str(x._id), "url" : x.url} +# tempJson["data"].append(tempJson1) +# return 200, tempJson #true->200 +# except Exception as ex: +# print(ex) +# return 400, {"error": str(ex)} #false->400 + +# """ +# * BlockCharacter create +# """ +# def create_block_character(user, location): +# try: +# blockCharacter = schema.BlockCharacter(location, ScopeClass.user.value, False, datetime.now(), user) +# result = schema.BlockCharacter.objects().insert(blockCharacter) +# return 200, {"id": str(result._id)} #true->200 +# except Exception as ex: +# print(ex) +# return 400, {"error": str(ex)} #false->400 + +# """ +# * UserBlockCharacter read +# """ +# def read_user_block_character(user): +# try: +# temp = schema.BlockCharacter.objects(user_id = ObjectId(user), scope = ScopeClass.user.value, is_deleted=False) +# tempJson = {} +# tempJson['data'] = [] +# for x in temp: +# tempJson1 = {"id" : str(x._id), "url" : x.url} +# tempJson['data'].append(tempJson1) +# return 200, tempJson +# except Exception as ex: +# print(ex) +# return 400, {"error": str(ex)} + +# """ +# * BlockCharacter delete +# """ +# def delete_block_character(user, _id): +# try: +# blockCharacter = schema.BlockCharacter.objects(_id = ObjectId(_id), scope = ScopeClass.user.value, user_id = user, is_deleted=False) +# if blockCharacter.count() == 0: +# return 404, {"error": f'{status_code.id_error}block_character'} #false->404 +# deleteBlockCharacter = blockCharacter.update( +# is_deleted=True, +# updated_at =datetime.now() +# ) +# if deleteBlockCharacter > 0: +# return 200, {"message": status_code.delete_01_success} #true->200 +# else: +# print("Can't be deleted") +# return 400, {"error": status_code.delete_02_fail} #false->400 +# except Exception as ex: +# print(ex) +# return 400, {"error": str(ex)} #false->400 + +# """ +# * BlockCharacter corresponding to id read +# """ +# def read_block_character_url(blockCharacterId): +# try: +# blockCharacter = schema.BlockCharacter.objects(_id = ObjectId(blockCharacterId), is_deleted=False).first() +# return True, blockCharacter.url +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} + +# ################################################ VIDEO ################################################ +# """ +# * Video create +# """ +# def create_video(user, location): +# try: +# video = schema.Video(user, location, ScopeClass.origin.value, datetime.now()) +# result = schema.Video.objects().insert(video) +# return True, str(result._id) +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} + +# """ +# * OriginVideo read +# """ +# def read_origin_video(_id, user): +# try: +# video = schema.Video.objects(_id = _id, user_id = user).first() +# if video.processed_url_id == None: +# return True, video.origin_url +# else: +# return False, {"error": status_code.celery_error} +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} + +# """ +# * ProccessedVideo read +# """ +# def read_proccessed_video(user): +# try: +# temp = schema.Video.objects(user_id = ObjectId(user), status = StatusClass.success.value) +# tempJson = {} +# tempJson['data'] = [] +# for x in temp: +# # 셀러리 id +# temp2 = schema.Celery.objects(_id = x.processed_url_id).first() +# if temp2 == None: +# continue +# tempJson1 = {"id" : str(x._id), "url" : temp2.result.replace('\"', '')} +# tempJson['data'].append(tempJson1) +# return True, tempJson +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} + +# """ +# * Video db update +# """ +# def update_video(_id, user, faceType, whitelistFace, blockCharacterId=""): +# try: +# if faceType != FaceTypeClass.character.value and faceType != FaceTypeClass.mosaic.value: +# print("Can't find face type") +# return False, {"error": f'{status_code.enum_class_error}face type'} +# video = schema.Video.objects(_id = ObjectId(_id), user_id = user) +# if video.count() == 0: +# return False, {"error": f'{status_code.id_error}video'} +# if blockCharacterId == "" or blockCharacterId == None: +# processedVideo = video.update( +# status = StatusClass.processed.value, +# face_type = faceType, +# whitelist_faces = whitelistFace, +# completed_at = datetime.now() +# ) +# else: +# processedVideo = video.update( +# status = StatusClass.processed.value, +# face_type = faceType, +# block_character_id = ObjectId(blockCharacterId), +# whitelist_faces = whitelistFace, +# completed_at = datetime.now() +# ) +# if processedVideo > 0: +# return True, {"message": status_code.update_01_success} +# else: +# print("Can't be modified") +# return False, {"error": status_code.update_02_fail} +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} + +# """ +# * Video celeryId update +# """ +# def update_video_celery(_id, user, task, status): +# try: +# # if status not in StatusClass: +# # print("Can't find stauts") +# # return False +# video = schema.Video.objects(_id = ObjectId(_id), user_id = user) +# if video == None: +# return False, {"error": f'{status_code.id_error}video'} +# processedVideo = video.update( +# status = status, +# processed_url_id = task.id, +# completed_at = datetime.now() +# ) +# if processedVideo > 0: +# return True, {"message" : status_code.update_01_success} +# else: +# print("Can't be modified") +# return False, {"error" : status_code.update_02_fail} +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} + +# """ +# * video delete +# """ +# def delete_video(user, _id): +# try: +# video = schema.Video.objects(_id = ObjectId(_id), user_id = user) +# if video.count() == 0: +# return False, {"error": f'{status_code.id_error}video'} +# deleteVideo = video.update( +# status = "deleted", +# updated_at = datetime.now() +# ) +# if deleteVideo > 0: +# return True, {"message" : status_code.delete_01_success} +# else: +# print("Can't be deleted") +# return False, {"error" : status_code.delete_02_fail} +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} + +# """ +# * read celery status +# """ +# def read_celery_status(user, taskId): +# try: +# temp = schema.Video.objects(user_id = user, processed_url_id = taskId).first() +# temp3 = schema.Celery.objects(_id = temp.processed_url_id).first() +# if temp3 != None: +# if temp3.status == StatusClass.success.value: +# temp2 = schema.Video.objects(user_id = user, processed_url_id = taskId).update(status = StatusClass.success.value) +# if temp2 > 0: +# return True, temp3.status +# else: +# # db 업데이트 실패 +# return False, {"error" : status_code.update_02_fail} +# elif temp3.status == StatusClass.failure.value: +# # failure일 경우 +# return True, temp3.status +# else: +# # pending일 경우 (셀러리 결과가 db에 저장되지 않음) +# return 0, StatusClass.pending.value +# except Exception as ex: +# print(ex) +# return False, {"error": str(ex)} + +# """ +# * video status update when the celery status is failure +# """ +# def update_video_celery_failure(user, taskId): +# video = schema.Video.objects(user_id = user, processed_url_id = taskId) +# if video.count() == 0: +# return False +# updateVideo = video.update(status = StatusClass.failure.value) +# if updateVideo > 0: +# return True +# else: +# return False \ No newline at end of file diff --git a/backend/module/file_module.py b/backend/module/file_module.py new file mode 100644 index 0000000..3f01f45 --- /dev/null +++ b/backend/module/file_module.py @@ -0,0 +1,81 @@ +from bucket.m_connection import minio_connection, minio_put_object +from bucket.m_config import BUCKET_NAME, MINIO_API_HOST +from datetime import datetime +from pytz import timezone +import random +import os +import subprocess + +""" +* 파일 업로드 +""" +def file_upload(request_id, result_id, collctionName, f): + try: + # 1. local에 파일 저장 - 파일 경로 때문에 저장해야함 + f.save(f.filename) + # subprocess.run(['ffmpeg', '-i', f.filename, + # '-acodec', 'pcm_s16le', '-ar', '48000', '-ac', '1', f.filename, '-y']) + + # 2. 파일명 설정 + name, ext = os.path.splitext(f.filename) + fileTime = datetime.now(timezone('Asia/Seoul')).strftime('%Y-%m-%d') + filename = f"{int(request_id):05}" + "_" + f"{int(result_id):05}" + "_voice_" + fileTime + ext + + # 3. 버킷 연결 + storage = minio_connection() + + # 4. 버킷에 파일 저장 + ret = minio_put_object(storage, f'{collctionName}/{filename}', f.filename) + location = f'https://{MINIO_API_HOST}/{BUCKET_NAME}/{collctionName}/{filename}' + + # 5. local에 저장된 파일 삭제 + os.remove(f.filename) + + # 6. 버킷에 파일 저장 성공 시 진행 + if ret : + # 6-3. 성공 message return + if location != None: + return 200, location #true->200 + else: + print("Can't find location") + return False, {"error":"Can't find location"} #false ->400 + + # 6. 버킷에 파일 저장 실패 시 진행 (ret == False 일 경우) + else: + return False, {"error":"Can't saved in minio bucket"} #false ->400 + + except Exception as ex: + print("******************") + print(ex) + print("******************") + return False, {"error" : str(ex)} #false -> 400 + +# """ +# * condition image 및 gif 파일 읽고 랜덤 선정 +# """ +# def read_random_condition(age, gender): +# try: +# # 1. age 반올림 +# age = round(age, -1) + +# # 2. 버킷 연결 +# storage = minio_connection() + +# # 3. 버킷에서 리스트 가져오기 +# ret = minio_list_object(storage, age, gender) + +# # 4. 버킷에서 리스트 가져오기 성공 시 랜덤 선정 +# if ret == False: +# return False, {"error":"Can't find list"} #false ->400 +# else: +# print(ret) +# choicejpg = random.choice(ret) +# idx = choicejpg.rindex('.') +# choicemp4 = choicejpg[:idx] + '.mp4' +# return True, {"image" : choicejpg, "gif" : choicemp4} + +# except Exception as ex: +# print("******************") +# print(ex) +# print("******************") +# return False, {"error" : str(ex)} #false -> 400 \ No newline at end of file diff --git a/backend/module/module_config.py b/backend/module/module_config.py new file mode 100644 index 0000000..3a2cc43 --- /dev/null +++ b/backend/module/module_config.py @@ -0,0 +1,7 @@ +from dotenv import load_dotenv +import os + +load_dotenv() + +SECRET_KEY = os.getenv('SECRET_KEY') +TOKEN_EXPIRED = int(os.getenv('TOKEN_EXPIRED')) diff --git a/backend/module/token.py b/backend/module/token.py new file mode 100644 index 0000000..0d75f53 --- /dev/null +++ b/backend/module/token.py @@ -0,0 +1,101 @@ +from module.module_config import SECRET_KEY, TOKEN_EXPIRED +from datetime import datetime, timedelta +from db import schema +import jwt +# from bson import ObjectId +from static import status_code + +def create_token(email): + try: + if email != None: + date = datetime.utcnow() + timedelta(seconds = TOKEN_EXPIRED) + payload = { + "email" : email, + "exp" : date + } + token = jwt.encode(payload, SECRET_KEY, algorithm = 'HS256') + if token == None: + print(status_code.user_auth_03_fail) + return False, {"error":status_code.user_auth_03_fail} + return True, token + else: + return False, {"error":status_code.user_auth_02_notmatch} + except Exception as ex: + print("***********") + print(ex) + print("***********") + return False, {"error": str(ex)} + +""" +* 토큰 유효성 검사 +* 로그인 유효성 검사가 필요한 함수 위에 @login_required를 넣으면 됨 +* 예시) @app.route('/action', methods = ["POST"]) +* @login_required +* def sample_action(): +* ### +* ### +* return Response() +* 수정이 필요할 수 있음 +* 페이지를 넘길 때 유효성 검사를 하려면 어떻게 해야.. +* 토큰이 만료된 경우 처음화면으로 돌아가게 할 수 있나? +* app.py에 적용 필요함 +""" +def decode_token(my_token): # 매개 변수로 토큰을 받아옴 + try: + # 1. payload 변수에 jwt토큰을 decode + payload = jwt.decode(my_token, SECRET_KEY, algorithms = "HS256") + + # 3. 부적절한 토큰인 경우 예외 발생 + except jwt.InvalidTokenError as err: + print(err) + print("Invalid token") + payload = None + return payload + else: + return payload + +def login_required(f, my_token): + @wraps(f) + def decorated_function(*args, **kwagrs): + # 1. 토큰을 가져옴 + # 추후에 가져오는 방법이 바뀔 수 있음 + # 프론트에서 JWT토큰을 헤더에 넣어 'my_token' 키로 전달 + # my_token = request.headers.get("my_token") + + # 2-1. 토큰을 가져오면 + #if my_token != None: + payload = decode_token(my_token) + # 2-2. 토큰 가져오는 것을 실패하면 + #else: + #print("Can't find token") + #return False + #return f(*args, **kwagrs) + return decorated_function + +""" +* 토큰으로부터 email 얻기 +* 수정이 필요할 수 있음 +* app.py에 적용 필요함 +""" +def get_user(my_token): + try: + payload = jwt.decode(my_token, SECRET_KEY, algorithms = "HS256") + + if payload is not None: + email = payload['email'] + user = schema.User.query.filter(schema.User.email == email, schema.User.deleted_at.is_(None)).first() + return user.id + else: + return False + + except jwt.InvalidTokenError as ex: + print("***********") + print(ex) + print("***********") + return False + except Exception as ex: + print("***********") + print(ex) + print("***********") + return False + \ No newline at end of file diff --git a/backend/module/user_module.py b/backend/module/user_module.py new file mode 100644 index 0000000..11dfb07 --- /dev/null +++ b/backend/module/user_module.py @@ -0,0 +1,223 @@ +from flask import request +import hashlib +from datetime import datetime +from pytz import timezone +from module.token import create_token +# from module.db_module import create_whitelist_face +from static import status_code +from db import schema +from db.db_connection import db +import module + +""" +* 이메일 중복체크 +""" +def email_validation(): + try: + email = request.form.get('email') + if email == None or email == '': + return 404, {"error": f'{status_code.field_error}email'} + user = schema.User.query.filter_by(email=email).first() + if not user: + print("This email is already exist") + return 409, {"error": status_code.user_email_validation_03_already} + else: + return 200, {"message": status_code.user_email_validation_01_success} + except Exception as ex: + print('*********') + print(ex) + print('*********') + return 400, {"error": str(ex)} + +""" +* 회원가입 +""" +def create_users(): + try: + email = request.form.get('email') + if email == None or email == '': + return 404, {"error": f'{status_code.field_error}email'} + password = request.form.get('password') + if password == None or password == '': + return 404, {"error": f'{status_code.field_error}password'} + age = request.form.get('age') + if age is None: + age = 20 + if not age.isdigit(): + return 404, {"error": f'{status_code.field_error}age'} + gender = request.form.get('gender') + if gender is None: + gender = 'man' + if gender != 'man' and gender != 'woman': + return 404, {"error": f'{status_code.field_error}gender'} + + # Check if email already exists + existing_user = schema.User.query.filter_by(email=email).first() + if existing_user: + return 409, {"error": status_code.user_email_validation_03_already} + + pwHash = hashlib.sha256(password.encode('utf-8')).hexdigest() + + # Creating a new User instance + new_user = schema.User(email=email, password=pwHash, age=age, gender=gender, created_at=datetime.now(timezone('Asia/Seoul'))) + + # Adding the new user to the session and committing to the database + db.session.add(new_user) + db.session.commit() + + return 201, {"id": new_user.email} + except Exception as ex: + db.session.rollback() + print('*********') + print(ex) + print('*********') + return 400, {"error": str(ex)} + +""" +* 로그인 +""" +def login(): + try: + email = request.form.get('email') + if email == None or email == '': + return 404, {"error": f'{status_code.field_error}email'} + password = request.form.get('password') + if password == None or password == '': + return 404, {"error": f'{status_code.field_error}password'} + + pwHash = hashlib.sha256(password.encode("utf-8")).hexdigest() + user = schema.User.query.filter_by(email=email, password=pwHash).first() + if not user: + return 404, {"error": "Can't find user"} + + result, token = create_token(user.email) + if result == False: + return 400, token + else: + return 200, {"token": token, "email": user.email} + except Exception as ex: + db.session.rollback() + print('*********') + print(ex) + print('*********') + return 400, {"error": str(ex)} + + +""" +* 유저 정보 받아오기: age, gender +""" +def get_user_info(): + try: + token = request.headers.get('Token') + user_id = module.token.get_user(token) + if user_id == False: + return 401, {"error": status_code.token_error} + + user = schema.User.query.filter_by(id=user_id).first() + + return 200, {"age" : str(user.age), "gender" : str(user.gender)} + except Exception as ex: + print('*********') + print(ex) + print('*********') + return 400, {"error": str(ex)} + +""" +* 이메일 찾기 +""" +# def find_email(): +# try: +# name = request.form.get('name') +# if name == None or name == '': +# return 404, {"error": f'{status_code.field_error}name'} +# phone = request.form.get('phone') +# if phone == None or phone == '': +# return 404, {"error": f'{status_code.field_error}phone'} + +# user = schema.User.objects(name = name, phone = phone).first() +# if user == None: +# print("Can't find user") +# return 404, {"error": status_code.user_email_find_02_fail} +# else: +# return 200, {"email": user.email} +# except Exception as ex: +# print('*********') +# print(ex) +# print('*********') +# return 404, {"error": str(ex)} + +''' +* 비밀번호 찾기 전 확인 +''' +# def password_validation(): +# try: +# email = request.form.get('email') +# if email == None or email == '': +# return 404, {"error": f'{status_code.field_error}email'} +# phone = request.form.get('phone') +# if phone == None or phone == '': +# return 404, {"error": f'{status_code.field_error}phone'} + +# user = schema.User.objects(email = email, phone = phone).first() +# if user == None: +# print("Can't find user") +# return 404, {"error": "Can't find user"} +# else: +# return 200, {"message": status_code.user_password_validation_01_success} +# except Exception as ex: +# print('*********') +# print(ex) +# print('*********') +# return 400, {"error": str(ex)} + +''' +* 비밀 번호 변경 +''' +# def update_password(): +# try: +# email = request.form.get('email') +# if email == None or email == '': +# return 404, {"error": f'{status_code.field_error}email'} +# password = request.form.get('password') +# if password == None or password == '': +# return 404, {"error": f'{status_code.field_error}password'} +# phone = request.form.get('phone') +# if phone == None or phone == '': +# return 404, {"error": f'{status_code.field_error}phone'} + +# pwHash = hashlib.sha256(password.encode('utf-8')).hexdigest() +# dbResponse = schema.User.objects(email = email, phone = phone).update(set__password = pwHash, set__updated_at = datetime.now()) +# if dbResponse > 0: +# return 200, {"message": status_code.user_password_replace_01_success} +# else: +# print("Can't be modified") +# return 400, {"error": status_code.user_password_replace_02_fail} +# except Exception as ex: +# print('*********') +# print(ex) +# print('*********') +# return 400, {"error": str(ex)} + +''' +* 회원탈퇴 +''' +# def delete_member(user_id): +# try: + +# # user_id는 token +# idReceive = get_id(user_id) + +# # 2. DB에서 유저아이디 일치하는 Document -> activation_YN = N +# dbResponse = schema.UploadCharacter.objects(user_id = idReceive).update(set__activation_YN = "N", set__mod_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + +# if dbResponse.modified_count > 0: +# return True +# else: +# print("Can't be modified") +# return False + +# except Exception as ex: +# print('*********') +# print(ex) +# print('*********') +# return False diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..ab1208b --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,19 @@ +flask +flask_restx +boto3 +awscli +Werkzeug +PyJWT +flask_migrate +# celery +pymysql +Flask-SQLAlchemy +SQLAlchemy +gunicorn +flask_cors +# prometheus_flask_exporter +python-dotenv +cryptography +celery[redis] +minio +pydub \ No newline at end of file diff --git a/backend/simple_worker/Dockerfile b/backend/simple_worker/Dockerfile new file mode 100644 index 0000000..2b88969 --- /dev/null +++ b/backend/simple_worker/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.8-slim + +# layer caching for faster builds +COPY requirements.txt / +RUN apt-get update && apt-get install -y \ + gcc \ + build-essential \ + && rm -rf /var/lib/apt/lists/* +RUN pip install --upgrade pip +RUN pip install -r /requirements.txt + +#COPY app.py /app.py +ADD . /simple_worker +WORKDIR /simple_worker + +CMD ["celery", "-A", "tasks", "worker", "-l", "info", "--pool=gevent", "--concurrency=4"] +#-Q celery_worker +#ENTRYPOINT ['celery','-A','test_celery', 'worker', '--loglevel=info'] \ No newline at end of file diff --git a/backend/simple_worker/db_config.py b/backend/simple_worker/db_config.py new file mode 100644 index 0000000..8b93ca3 --- /dev/null +++ b/backend/simple_worker/db_config.py @@ -0,0 +1,9 @@ +from dotenv import load_dotenv +import os + +load_dotenv() + +USERNAME = os.getenv('USERNAME') +PASSWORD = os.getenv('PASSWORD') +HOST = os.getenv('HOST') +DATABASE = os.getenv('DATABASE') diff --git a/backend/simple_worker/db_connection.py b/backend/simple_worker/db_connection.py new file mode 100644 index 0000000..4e86c61 --- /dev/null +++ b/backend/simple_worker/db_connection.py @@ -0,0 +1,38 @@ +import pymysql +from db_config import USERNAME, PASSWORD, HOST, DATABASE +from datetime import datetime +from pytz import timezone + +class Database(): + def __init__(self): + self.db = pymysql.connect( + host=HOST[:-5], + port=3306, + user=USERNAME, + passwd=PASSWORD, + db=DATABASE, + init_command='SET SESSION wait_timeout = 86400' + ) + self.cursor = self.db.cursor() + + def update_mz_request_status_ata(self, request_id, status_to_change): + self.cursor.execute('UPDATE mz_request \ + SET status = %s, \ + ata = %s \ + WHERE id = %s', (status_to_change, str(datetime.now(timezone('Asia/Seoul'))), request_id)) + self.db.commit() + + + def update_mz_result_image_gif(self,mz_request_id, task_result): + condition_image_url = task_result['condition_image_url'] + condition_gif_url = task_result['condition_gif_url'] + voice_image_url = task_result['voice_image_url'] + voice_gif_url = task_result['voice_gif_url'] + + self.cursor.execute('UPDATE mz_result \ + SET condition_image_url = %s, \ + condition_gif_url = %s, \ + voice_image_url = %s, \ + voice_gif_url = %s \ + WHERE mz_request_id = %s', (condition_image_url, condition_gif_url, voice_image_url, voice_gif_url, mz_request_id)) + self.db.commit() \ No newline at end of file diff --git a/backend/simple_worker/minio_config.py b/backend/simple_worker/minio_config.py new file mode 100644 index 0000000..29713af --- /dev/null +++ b/backend/simple_worker/minio_config.py @@ -0,0 +1,9 @@ +from dotenv import load_dotenv +import os + +load_dotenv() + +ACCESS_KEY = os.getenv("AWS_ACCESS_KEY") +SECRET_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") +BUCKET_NAME = os.getenv("MINIO_BUCKET") +MINIO_API_HOST = os.getenv("MINIO_ENDPOINT") \ No newline at end of file diff --git a/backend/simple_worker/minio_connection.py b/backend/simple_worker/minio_connection.py new file mode 100644 index 0000000..a3bf25a --- /dev/null +++ b/backend/simple_worker/minio_connection.py @@ -0,0 +1,82 @@ +from minio import Minio +#from minio.error import ResponseError +from minio_config import ACCESS_KEY, SECRET_KEY +from minio_config import BUCKET_NAME, MINIO_API_HOST +import random + +def minio_connection(): + try: + storage = Minio( + MINIO_API_HOST, + ACCESS_KEY, + SECRET_KEY, + secure=True, + ) + except Exception as e: + print(e) + return False + else: + print("storage bucket connected!") + return storage + +def minio_list_object(storage, age, gender): + ''' + minio bucket에서 해당 성별과 나이에 맞는 이미지 리스트 가져오기 + :param storage: 연결된 minio 객체(Minio client) + :param prefix: Object name starts with prefix. + :param age: 나이 + :param gender: 성별 + :return: 성공 시 list 반환, 실패 시 False 반환 + ''' + try: + prefix = "output_condition/" + obj_list = list(storage.list_objects(BUCKET_NAME, prefix)) + file_list = [obj.object_name for obj in obj_list] + condition_file_list = [] + for file in file_list: + _, file_name = file.split('-') + idx = file_name.rindex('.') + if file_name[idx+1:] == 'jpg' and file_name[:idx] == f'{gender}_{age}': + condition_file_list.append(f"https://{MINIO_API_HOST}/{BUCKET_NAME}/{file}") + except Exception as e: + print(e) + return False + return condition_file_list + +def read_random_condition(age, gender): + try: + # 1. age 반올림 + age = round(int(age), -1) + if age == 50: + age = 40 + + # 2. 버킷 연결 + storage = minio_connection() + + # 3. 버킷에서 리스트 가져오기 + ret = minio_list_object(storage, age, gender) + + # 4. 버킷에서 리스트 가져오기 성공 시 랜덤 선정 + if ret == False: + return False, {"error":"Can't find list"} #false ->400 + else: + print(ret) + choicejpg = random.choice(ret) + idx = choicejpg.rindex('.') + choicemp4 = choicejpg[:idx] + '.mp4' + return True, {"image" : choicejpg, "gif" : choicemp4} + + except Exception as ex: + print("******************") + print(ex) + print("******************") + return False, {"error" : str(ex)} #false -> 400 + +# def check_object_existence(storage, bucket_name, object_name): +# try: +# # 객체 존재 여부 확인 +# storage.stat_object(bucket_name, object_name) +# return True +# except ResponseError as err: +# if err.code == 'NoSuchKey': +# return False \ No newline at end of file diff --git a/backend/simple_worker/requirements.txt b/backend/simple_worker/requirements.txt new file mode 100644 index 0000000..2d63ad4 --- /dev/null +++ b/backend/simple_worker/requirements.txt @@ -0,0 +1,9 @@ +celery[redis] +pymysql +Flask-SQLAlchemy +SQLAlchemy +python-dotenv +requests +minio +gevent +pytz \ No newline at end of file diff --git a/backend/simple_worker/tasks.py b/backend/simple_worker/tasks.py new file mode 100644 index 0000000..ff12811 --- /dev/null +++ b/backend/simple_worker/tasks.py @@ -0,0 +1,94 @@ +import time +from celery import Celery +from celery.utils.log import get_task_logger +import requests +from db_config import USERNAME, PASSWORD, HOST, DATABASE +import requests +from minio_connection import read_random_condition +from db_connection import Database +import json + +logger = get_task_logger(__name__) + +# Create a Celery instance named 'celery_app' +# celery = Celery('tasks',backend='db+mysql+pymysql://root:rootpwd@mysql-db:3306/MZ', broker='amqp://admin:mypass@rabbit:5672') +celery = Celery('tasks',backend=f'db+mysql+pymysql://{USERNAME}:{PASSWORD}@{HOST}/{DATABASE}', broker='amqp://admin:mypass@rabbit:5672') +db = Database() + +@celery.task() +def run_mz(request_id, result_id, age, gender, file_url): + logger.info('Got Request - Starting work') + time.sleep(4) + + target_server_url = 'http://175.45.193.25:3002/makevideo' + data = {'age' : age, + 'gender' : gender, + 'voice_url': file_url, + 'request_id' : request_id, + 'result_id' : result_id} + logger.info(data) + condition_image_url = None + condition_video_url = None + voice_image_url = None + voice_video_url = None + return_message = 200 + + try: + # condition output + result, message = read_random_condition(age, gender) + if result: + condition_image_url = str(message['image']) + condition_video_url = str(message['gif']) + logger.info(condition_image_url) + logger.info(condition_video_url) + else: + raise Exception('condition') + + # voice output + headers = {"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36","Content-Type":"application/json"} + response = requests.post(target_server_url,data=json.dumps(data),headers=headers) + response_data = response.json() + status_code = response_data.get("status_code") + logger.info(status_code) + + if status_code == 200: # 성공 시 + voice_image_url = str(response_data.get("voice_image_url")) + voice_video_url = str(response_data.get("voice_video_url")) + logger.info(voice_image_url) + logger.info(voice_video_url) + elif status_code == 404: # voice_image_url 만 받아온 경우 + voice_image_url = str(response_data.get("voice_image_url")) + logger.info(voice_image_url) + error = response_data.get("error") + raise Exception(error) + else: # 실패 시 + # Update status + error = response_data.get("error") + raise Exception(error) + + #except requests.RequestException as e: + except Exception as e: + logger.info(str(e)) + return_message = f'failed with exception: {str(e)}' + + # Update result + result_to_change = { + 'condition_image_url' : condition_image_url, + 'condition_gif_url' : condition_video_url, + 'voice_image_url' : voice_image_url, + 'voice_gif_url' : voice_video_url + } + logger.info(result_to_change) + db.update_mz_result_image_gif(request_id, result_to_change) + + if None in [condition_image_url, condition_video_url, voice_image_url]: + status_to_change = 'Failed' + else: + status_to_change = 'Success' + logger.info(status_to_change) + db.update_mz_request_status_ata(request_id, status_to_change) + + logger.info('Work Finished ') + # return response.status_code + return return_message + diff --git a/backend/static/status_code.py b/backend/static/status_code.py new file mode 100644 index 0000000..2246ed3 --- /dev/null +++ b/backend/static/status_code.py @@ -0,0 +1,72 @@ +#######################CRUD####################### +# create code +create_01_success = "create success" +create_02_fail = "create fail" + +# read code +read_01_success = "read success" +read_02_fail = "read fail" + +# update code +update_01_success = "update success" +update_02_fail = "update fail" + +# delete code +delete_01_success = "delete success" +delete_02_fail = "delete fail" + +#######################ERROR####################### +# token error code +token_error = {"code": "TK02", "message": "Invalid token or can't find user. Please check the token"} + +# field error code +field_error = "Can't find field: " + +# enum class error code +enum_class_error = "Can't find enum class element: " + +# _id error code +id_error = "Can't find _id or invalid _id: " + +# celery error code +celery_error = "Already processed" + +#######################AI####################### +# push celery code +celery_push_01_success = "celery push success" +celery_push_02_fail = "celery push fail" + +# read code +read_celery_status_01_success = "read celery status success" +read_celery_status_02_fail = "read celery status fail" + +######################USER###################### +# email validation code +user_email_validation_01_success = "user email validation success" +user_email_validation_02_fail = "user email validation fail" +user_email_validation_03_already = "This email is already exist" + +# signup code +user_signup_01_success = "user signup success" +user_signup_02_fail = "user signup fail" + +# auth code +user_auth_01_success = "user auth success" +user_auth_02_notmatch = "user auth notmatch" +user_auth_03_fail = "user auth fail" + +# find email code +user_email_find_01_success = "user email find success" +user_email_find_02_fail = "user email find fail" + +# password validation code +user_password_validation_01_success = "user password validation success" +user_password_validation_02_fail = "user password validation fail" + +# replace password code +user_password_replace_01_success = "user password replace success" +user_password_replace_02_fail = "user password replace fail" + +# delete user code +user_delete_01_success = "user delete success" +user_delete_01_fail = "user delete fail" diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..360c808 --- /dev/null +++ b/data/README.md @@ -0,0 +1,138 @@ + + + +# 🔊 Voice2Face-Data + + + + + + + +## Project Structure + +``` +data +┣ audio +┃ ┣ audio_check_dB.py +┃ ┣ audio_dB_crop.py +┃ ┗ audio_wav_cropping.py +┣ crawling +┃ ┣ crawling_detect.py +┃ ┣ crawling_rename_video.py +┃ ┣ crawling_select_csv.py +┃ ┣ crawling_urlsave.py +┃ ┗ crawling_videosave.py +┣ image +┃ ┣ image_clipseg2.py +┃ ┗ image_face_frame.py +┣ relabel +┃ ┣ relabel_detect_getframe.py +┃ ┣ relabel_select_csv.py +┃ ┗ relabel_Vox_age.py +┣ total +┃ ┣ total_audio_video_image.py +┃ ┗ total_origin_remove.py +┣ video +┃ ┣ video_clipimage.py +┃ ┗ video_download.py +┣ README.md +┗ requirements.txt +``` +## Usage + + + +#### audio + - `audio_check_dB.py`: 특정 dB 값을 확인하여 사람의 음성 여부를 판별하는 스크립트입니다. + - `audio_dB_crop.py`: 오디오 파일에서 인간의 목소리 세그먼트를 추출하고 감지된 음성 세그먼트를 포함하는 새로운 오디오 파일을 10초로 자르는 스크립트입니다. + - `audio_wav_cropping.py`: JSON POINT에 맞춰 오디오를 자르는 스크립트입니다. + +#### crawling + + - `crawling_detect.py`: 비디오 클립에서 얼굴과 오디오를 감지하고 세분화하는 스크립트입니다. + - `crawling_rename_video.py`: 'download' 폴더에서 비디오 이름과 CSV의 인덱스를 맞추는 스크립트입니다. + - `crawling_select_csv.py`: 주어진 CSV 파일에서 YouTube ID를 찾아 해당하는 파일 이름에서 정보를 추출하고, 이 정보를 새로운 CSV 파일에 저장하는 간단한 데이터 처리 작업을 수행하는 스크립트입니다. + - `crawling_urlsave.py`: Selenium을 사용하여 YouTube 크롤링을 수행하여 약 162개의 비디오에 대한 이름, 제목 및 URL 정보를 Youtube_search_df.csv에 저장하는 스크립트입니다. + - `crawling_videosave.py`: '`crawling_urlsave.py`'를 통해 얻은 URL에서 비디오를 다운로드하는 스크립트입니다. 비디오는 'download' 폴더에 저장됩니다. + +#### image + + - `image_clipseg2.py`: CLIPSeg 모델을 사용하여 텍스트 프롬프트를 기반으로 이미지 세분화를 수행하는 스크립트입니다. 이미지를 불러와 텍스트 프롬프트로 처리하고, 식별된 객체를 기반으로 세분화된 이미지를 생성합니다. + - `image_face_frame.py`: 비디오에서 사람의 얼굴이 정면이고, 눈을 뜨고 있을 때 캡쳐하고 배경을 제거하는 스크립트입니다. + +#### relabel + + - `relabel_detect_getframe.py`: 주어진 비디오에서 얼굴을 감지하고, 감지된 얼굴에 대해 성별과 연령을 추정하여 화면에 표시하고, 일정한 간격으로 프레임을 캡처하여 이미지 파일로 저장하는 기능을 수행합니다. + - `relabel_select_csv.py`: 데이터 경로에서 YouTube ID를 추출하고, 파일 이름에서 필요한 정보를 추출하여 새로운 CSV 파일에 저장하는 스크립트입니다. + - `relabel_Vox_age.py`: 이미지 폴더에서 이미지들을 읽어와 각 이미지의 나이를 예측하고, 가장 흔한 나이 그룹을 세서 출력하고, 그 결과를 CSV 파일에 저장하는 작업을 수행합니다. + +#### video + + - `video_clipimage.py`: 주어진 이미지에서 얼굴을 감지하고, 감지된 얼굴 영역을 사각형으로 표시한 후 해당 얼굴을 256x256 크기로 조정하여 저장하는 작업을 수행합니다. + - `video_download.py`: 주요 기능은 주어진 YouTube 비디오 링크에서 비디오를 다운로드하고, 다운로드한 비디오를 mp4 또는 mp3 형식으로 변환하는 스크립트입니다. + +#### total +- `total_audio_video_image.py`: 오디오, 비디오 및 이미지와 관련된 작업을 총 수행하는 스크립트입니다. +- `total_origin_remove.py`: 데이터 경로에서 원본 파일을 제거하는 스크립트입니다. + + + + +## Getting Started + + +### Setting up Vitual Enviornment + + +1. Initialize and update the server + +``` + +su - + +source .bashrc + +``` + + + +2. Create and Activate a virtual environment in the project directory + + + +``` + +conda create -n env python=3.8 + +conda activate env + +``` + + + +4. To deactivate and exit the virtual environment, simply run: + + + +``` + +deactivate + +``` + + + +### Install Requirements + + + +To Install the necessary packages liksted in `requirements.txt`, run the following command while your virtual environment is activated: + +``` + +pip install -r requirements.txt + +``` + + \ No newline at end of file diff --git a/data/audio/audio_check_dB.py b/data/audio/audio_check_dB.py new file mode 100644 index 0000000..a1a4bda --- /dev/null +++ b/data/audio/audio_check_dB.py @@ -0,0 +1,52 @@ +import librosa +import numpy as np +import matplotlib.pyplot as plt + +''' +You can determine the minimum, maximum, and average dB values +to set a threshold for identifying voice regions based on dB levels. +After visually inspecting the waveform and setting a threshold, +adding 80 to it, you can conveniently apply this threshold value to `audio_crop.py`. +''' + +# Load audio file +audio_path = "voice2face-data/audio/input.wav" +y, sr = librosa.load(audio_path, sr=None) + +# Calculate spectrum and check maximum and minimum dB values +D = librosa.amplitude_to_db(librosa.stft(y), ref=np.max) +max_db = np.max(D) +min_db = np.min(D) + +# Set threshold value +threshold_db = -60 + +# Consider regions with dB values above the threshold as voice regions +voice_indices = np.where(D > threshold_db) + +print("Threshold:", threshold_db) +print("Maximum dB value in regions with voice:", np.max(D[voice_indices])) +print("Minimum dB value in regions with voice:", np.min(D[voice_indices])) + +# Calculate average dB value in regions with voice +average_db = np.mean(D[voice_indices]) +print("Average dB value in regions with voice:", average_db) + +# Plot waveform and spectrum +plt.figure(figsize=(12, 6)) + +# Plot waveform +plt.subplot(2, 1, 1) +plt.plot(y) +plt.title("Waveform") +plt.xlabel("Sample") +plt.ylabel("Amplitude") + +# Plot spectrum +plt.subplot(2, 1, 2) +librosa.display.specshow(D, sr=sr, x_axis='time', y_axis='log') +plt.colorbar(format='%+2.0f dB') +plt.title('Log-frequency power spectrogram') + +plt.tight_layout() +plt.show() diff --git a/data/audio/audio_dB_crop.py b/data/audio/audio_dB_crop.py new file mode 100644 index 0000000..8bb3f58 --- /dev/null +++ b/data/audio/audio_dB_crop.py @@ -0,0 +1,107 @@ +import os +import librosa +import soundfile as sf +import matplotlib.pyplot as plt +from pydub import AudioSegment + +''' +Extracts human voice segments from an audio file and creates a new audio file with the detected voice segments +within a 10-second duration. + +Args: + audio_file (str): Path to the input audio file. If the file format is .m4a, it will be converted to .wav. + +Returns: + save_file (str): Path to the saved audio file with detected voice segments. +''' + +def detect_human_voice(audio_file): + ''' + Detects human voice segments in an audio file. + + Args: + audio_file (str): Path to the input audio file. + + Returns: + voice_indices (list): List containing indices of the detected voice segments. + ''' + # Read the audio file + y, sr = librosa.load(audio_file, sr=None) + + # Detect voice activity + # ----- Need to Modify threshold-----# + voice_segments = librosa.effects.split(y, top_db=18) + + # Generate indices of voice segments + voice_indices = [] + for start, end in voice_segments: + voice_indices.extend(range(start, end)) + + return voice_indices + +def save_full_audio_with_detected_voice(audio_file, save_file): + ''' + Saves the full audio file with detected voice segments. + + Args: + audio_file (str): Path to the input audio file. + save_file (str): Path to save the audio file with detected voice segments. + ''' + # Read the entire audio file + y, sr = librosa.load(audio_file, sr=None) + + # Detect human voice segments and get their indices + voice_indices = detect_human_voice(audio_file) + + # Extract human voice segments using the indices + combined_audio = y[voice_indices] + + # Save the extracted audio segments to a file + sf.write(save_file, combined_audio, sr) + + # Visualize and save the waveform of the original and detected voice segments + plt.figure(figsize=(12, 6)) + + # Original audio waveform + plt.subplot(2, 1, 1) + plt.plot(y) + plt.title("Original Audio Waveform") + plt.xlabel("Sample") + plt.ylabel("Amplitude") + + # Waveform of detected voice segments + plt.subplot(2, 1, 2) + plt.plot(combined_audio) + plt.title("Detected Voice Waveform") + plt.xlabel("Sample") + plt.ylabel("Amplitude") + + plt.tight_layout() + save_path = os.path.join(os.path.dirname(save_file), 'result') + if not os.path.exists(save_path): + os.makedirs(save_path) + save_file_path = os.path.join(save_path, os.path.basename(save_file[:-4] + "_waveform_comparison.png")) + plt.savefig(save_file_path) + + # Save the extracted audio segments to a file + audio_save_file_path = os.path.join(save_path, os.path.basename(save_file)) + sf.write(audio_save_file_path, combined_audio, sr) + + plt.show() + +# Define paths for the original file and the file to save with detected voice segments +# ------Need to modify path------ # +audio_file_path = "voice2face-data/audio/input.m4a" +save_file_path = "voice2face-data/audio/detected_voice.wav" + +# Check if the file extension is ".m4a" for conversion and processing +if audio_file_path.endswith('.m4a'): + # Convert m4a file to wav format + wav_path = audio_file_path[:-4] + ".wav" + audio = AudioSegment.from_file(audio_file_path) + audio.export(wav_path, format="wav") + # Process the converted wav file + save_full_audio_with_detected_voice(wav_path, save_file_path) +else: + # Process the original file without conversion + save_full_audio_with_detected_voice(audio_file_path, save_file_path) diff --git a/data/audio/audio_wav_cropping.py b/data/audio/audio_wav_cropping.py new file mode 100644 index 0000000..fc52070 --- /dev/null +++ b/data/audio/audio_wav_cropping.py @@ -0,0 +1,107 @@ +import os +import json +from pydub import AudioSegment + +def process_wav_and_json(wav_file: str, json_file: str, wav_file_name: str, save_folder: str): + """ wav 파일을 받아 json 에서 필요한 부분을 잘라내 형태에 맞게 저장할 코드 + + Args: + wav_file (str): json 파일과 매칭되는 wav_file 경로 + json_file (str): wav_file croping에 사용될 json_file 경로 + wav_file_name (str): folder명 지정에 사용할 wav_file 자체 이름 + save_folder (str): 정제된 파일을 저장할 경로 + """ + count = 1 + audio = AudioSegment.from_wav(wav_file) + + # 저장할 폴더 생성 + wav_file_folder = wav_file_name.split("_")[:-2] + wav_file_folder = "_".join(wav_file_folder) + wav_file_folder = os.path.join(save_folder,'data', wav_file_folder,'audio') + os.makedirs(wav_file_folder, exist_ok=True) + + # json 파일 열기 + with open(json_file, 'r') as f: + data = json.load(f) + data= data[0] + + # 각 시간대별로 돌아가며 동영상 croping + for sentence_info in data['Sentence_info']: + start_time = round(int(sentence_info['start_time']), 3) + end_time = round(int(sentence_info['end_time']), 3) + time = end_time - start_time + if time <3 or time > 10 : + continue + + start_time_ms = start_time * 1000 + end_time_ms = end_time * 1000 + + + segment = audio[start_time_ms:end_time_ms] + output_wav_file = f"{wav_file_name.split('_')[-1]}_{count:03d}.wav" + + output_wav_path = os.path.join(wav_file_folder, output_wav_file) + + segment.export(output_wav_path, format='wav') + count += 1 + + print(f"Saved {output_wav_path}") + + +def find_matching_json(file_name: str, json_folder: str) ->str : + """wav 파일과 파일명이 똑같은 json 파일을 찾아 경로 반환, + 만약 없다면 None 반환 + + Args: + file_name (str): wav 파일 이름 가져오기 + json_folder (str): json이 저장된 위치 확인 + + Returns: + str: wav 파일에 해당하는 json 파일 반환 + """ + + for root, dirs, files in os.walk(json_folder): + for file in files: + if file.split('.')[0] == file_name: + json_file = os.path.join(root, file) + return json_file + return None + +def find_wav(wav_folder: str, json_folder: str, save_folder: str): + """ 처음 정제할 wav 파일 찾기 + 이후 json 파일을 찾아 wav를 정제한다. + + Args: + wav_folder (str): wav 파일들이 저장된 폴더 위치 + json_folder (str): json 파일들이 저장된 폴더 위치 + save_folder (str): croping 된 파일들을 저장할 위치 + """ + + for root,_,files in os.walk(wav_folder): + for file in files: + if file.endswith('.wav'): + wav_file = os.path.join(root, file) + wav_file_name = wav_file.split('/')[-1].split('.')[0] + json_file = find_matching_json(wav_file_name, json_folder) + + if json_file: + process_wav_and_json(wav_file, json_file, wav_file_name, save_folder) + os.remove(wav_file) + + +def main(wav_folder: str, json_folder: str, save_folder: str): + find_wav(wav_folder, json_folder, save_folder) + print('End of processing') + + + + +if __name__ == '__main__': + # WAV 파일이 들어있는 폴더 경로 + wav_folder = '/home/carbox/Desktop/data/009.립리딩(입모양) 음성인식 데이터/01.데이터/2.Validation/원천데이터' + # JSON 파일이 들어있는 폴더 경로 + json_folder = '/home/carbox/Desktop/data/009.립리딩(입모양) 음성인식 데이터/01.데이터/2.Validation/라벨링데이터' + # 원천데이터를 저장할 폴더 경로 + save_folder = '/home/carbox' + + main(wav_folder, json_folder, save_folder) diff --git a/data/crawling/crawling_detect.py b/data/crawling/crawling_detect.py new file mode 100644 index 0000000..917859f --- /dev/null +++ b/data/crawling/crawling_detect.py @@ -0,0 +1,139 @@ +import os +import pandas as pd +from moviepy.editor import VideoFileClip +import numpy as np +import face_recognition +import shutil + +''' +Detects faces and audio in video clips and refines them. + +Extracts faces from the video clips and selects segments with audio to rebuild new videos. +New videos are organized in the "processed_videos" folder. + +''' + +# Function to extract audio from video clips with detected faces +def extract_audio_with_face(video_clip, start_time, end_time): + ''' + Extracts audio from a video clip with detected faces within a specified time range. + + Args: + video_clip (VideoFileClip): Input video clip. + start_time (float): Start time of the segment containing the detected faces. + end_time (float): End time of the segment containing the detected faces. + + Returns: + audio (AudioClip): Extracted audio clip. + ''' + audio = video_clip.audio.subclip(start_time, end_time) + return audio + +# Function to extract audio from video clips with detected faces in multiple segments +def extract_audio_with_faces(video_clip, face_detections): + ''' + Extracts audio from a video clip with detected faces in multiple segments. + + Args: + video_clip (VideoFileClip): Input video clip. + face_detections (list): List of tuples containing start and end times of segments with detected faces. + + Returns: + final_audio (ndarray): Concatenated audio array from all detected face segments. + ''' + audio_clips = [] + + for start_time, end_time in face_detections: + audio_clip = extract_audio_with_face(video_clip, start_time, end_time) + audio_clips.append(audio_clip) + + final_audio = np.concatenate([clip.to_soundarray() for clip in audio_clips]) + return final_audio + +# Function to detect faces in video clips +def detect_faces(video_clip): + ''' + Detects faces in a video clip. + + Args: + video_clip (VideoFileClip): Input video clip. + + Returns: + face_detections (list): List of tuples containing start and end times of segments with detected faces. + ''' + frames = [frame for frame in video_clip.iter_frames()] + frame_rate = video_clip.fps + frame_times = np.arange(len(frames)) / frame_rate + face_detections = [] + + for i, frame in enumerate(frames): + face_locations = face_recognition.face_locations(frame) + if face_locations: + start_time = frame_times[max(0, i - 1)] + end_time = frame_times[min(len(frames) - 1, i + 1)] + face_detections.append((start_time, end_time)) + + return face_detections + +# Function to create a new video from detected face segments +def create_new_video(video_clip, face_detections, output_path): + ''' + Creates a new video from detected face segments. + + Args: + video_clip (VideoFileClip): Input video clip. + face_detections (list): List of tuples containing start and end times of segments with detected faces. + output_path (str): Path to save the new video. + ''' + new_video_clip = None + + for start_time, end_time in face_detections: + subclip = video_clip.subclip(start_time, end_time) + if new_video_clip is None: + new_video_clip = subclip + else: + new_video_clip = new_video_clip.append(subclip) + + new_video_clip.write_videofile(output_path) + +# Read data from a CSV file +csv_file_path = "/Users/imseohyeon/Documents/crawling/data/Youtube_search_df.csv" +df = pd.read_csv(csv_file_path) + +# Paths for input and output folders +DOWNLOAD_FOLDER = "/Users/imseohyeon/Documents/crawling/download/" +NEW_FOLDER = "/Users/imseohyeon/Documents/crawling/processed_videos/" + +# Create a new folder if it doesn't exist +if not os.path.exists(NEW_FOLDER): + os.makedirs(NEW_FOLDER) + +# Process each video to extract audio from segments with detected faces and create new videos +for idx, row in df.iterrows(): + video_filename = f"{idx}_video.mp4" + video_path = os.path.join(DOWNLOAD_FOLDER, video_filename) + + if os.path.exists(video_path): + try: + video_clip = VideoFileClip(video_path) + face_detections = detect_faces(video_clip) + + if face_detections: + final_audio = extract_audio_with_faces(video_clip, face_detections) + output_path = os.path.join(NEW_FOLDER, f"{idx}_new_video.mp4") + create_new_video(video_clip, face_detections, output_path) + + print(f"Processing complete for {video_filename}") + else: + print(f"No faces detected in {video_filename}") + except Exception as e: + print(f"Error processing {video_filename}: {e}") + else: + print(f"File {video_filename} does not exist.") + +# Move processed videos to another folder +processed_files = os.listdir(NEW_FOLDER) +for file in processed_files: + shutil.move(os.path.join(NEW_FOLDER, file), DOWNLOAD_FOLDER) + +print("All videos processed") diff --git a/data/crawling/crawling_rename_video.py b/data/crawling/crawling_rename_video.py new file mode 100644 index 0000000..3c005ad --- /dev/null +++ b/data/crawling/crawling_rename_video.py @@ -0,0 +1,30 @@ +import os +import pandas as pd + +''' +Match the video names in the 'download' folder with the index in the CSV. +This facilitates the subsequent video relabeling task. +''' + +# Read links from the CSV file +csv_file_path = "/Users/imseohyeon/Documents/crawling/data/Youtube_search_df.csv" +df = pd.read_csv(csv_file_path) + +# Path to the folder where downloaded videos are stored +DOWNLOAD_FOLDER = "/Users/imseohyeon/Documents/crawling/download/" + +# Iterate over all files in the folder and rename them +for filename in os.listdir(DOWNLOAD_FOLDER): + # Full path of the file + file_path = os.path.join(DOWNLOAD_FOLDER, filename) + # Check if the file is a .mp4 file + if filename.endswith(".mp4"): + # Extract the index value from the file name (assuming the video title is stored as the index) + idx = filename.split("_")[0] # Example: "0_video.mp4" -> "0" + # Create a new file name + new_filename = f"{idx}_video.mp4" + # Create the new file path + new_file_path = os.path.join(DOWNLOAD_FOLDER, new_filename) + # Rename the file + os.rename(file_path, new_file_path) + print(f"File renamed: {filename} -> {new_filename}") diff --git a/data/crawling/crawling_select_csv.py b/data/crawling/crawling_select_csv.py new file mode 100644 index 0000000..64097a7 --- /dev/null +++ b/data/crawling/crawling_select_csv.py @@ -0,0 +1,74 @@ +import os +import pandas as pd +from argparse import ArgumentParser + +def parse_args(): + """ + Command-line arguments parser. + + Returns: + argparse.Namespace: Parsed arguments. + """ + parser = ArgumentParser() + + parser.add_argument('--csv_file', type=str, default='output_test.csv', + help="Path to the CSV file containing data.") + parser.add_argument('--data_path', type=str, default='origin/video', + help="Path to the directory containing files and folders.") + parser.add_argument('--save_csv', type=str, default='new_output.csv', + help="Path to save the new CSV file.") + + args = parser.parse_args() + return args + +def list_files_and_folders(data_path): + """ + List files and folders in the given directory path. + + Args: + data_path (str): Path to the directory. + + Returns: + list or None: List of files and folders if directory exists, otherwise None. + """ + if os.path.isdir(data_path): + items = os.listdir(data_path) + return items + else: + return None + +def main(csv_file, data_path, save_csv): + """ + Main function to process data. + + Args: + csv_file (str): Path to the CSV file. + data_path (str): Path to the directory containing files and folders. + save_csv (str): Path to save the new CSV file. + """ + # Read CSV file + csv_data = pd.read_csv(csv_file, header=None) + + # Get list of files and folders in the data path + youtube_ids = list_files_and_folders(data_path) + + # Process each YouTube ID + for youtube_id in youtube_ids: + # Filter rows corresponding to the YouTube ID + filtered_df = csv_data[csv_data[0].astype(str).str.contains(youtube_id)] + first_row = filtered_df.iloc[0:1] + + # Extract information from file name + file_name = list_files_and_folders(os.path.join(data_path, youtube_id))[0] + file_name_list = file_name.split("_") + + # Add extracted information as new columns + first_row[4] = file_name_list[0] + first_row[5] = file_name_list[1] + + # Save to new CSV file + first_row.to_csv(save_csv, mode="a", index=False, header=False) + +if __name__ == '__main__': + args = parse_args() + main(**args.__dict__) diff --git a/data/crawling/crawling_urlsave.py b/data/crawling/crawling_urlsave.py new file mode 100644 index 0000000..36d9c8d --- /dev/null +++ b/data/crawling/crawling_urlsave.py @@ -0,0 +1,97 @@ +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys + +import requests +from bs4 import BeautifulSoup +import time +import pandas as pd +import os + +''' +YouTube crawling using Selenium + +Saves information of approximately 162 videos including name, title, and URL to Youtube_search_df.csv. +''' + +# Initialize WebDriver (executable_path not required as it's added to PATH) +browser = webdriver.Chrome() + +# URL to access +url = "https://youtube.com/" + +# Search keyword +keyword = "Solo Travel" + +# Scroll until the specified line +# finish_line = 40000 (about 162 videos) +finish_line = 10000 + +browser.maximize_window() +browser.get(url) +time.sleep(2) +search = browser.find_element(By.NAME, "search_query") +time.sleep(2) +search.send_keys(keyword) +search.send_keys(Keys.ENTER) + +# Switch to search result page for parsing +present_url = browser.current_url +browser.get(present_url) +last_page_height = browser.execute_script("return document.documentElement.scrollHeight") + +# Scroll 100 times +scroll_count = 0 +while scroll_count < 100: + # Scroll down + browser.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);") + time.sleep(2.0) # Waiting for data to load when scrolling down + new_page_height = browser.execute_script("return document.documentElement.scrollHeight") + + # Increase scroll count + scroll_count += 1 + +html_source = browser.page_source +soup = BeautifulSoup(html_source, 'html.parser') + +# Retrieve all search results up to the finish line +# Extract all content-related sections +elem = soup.find_all("ytd-video-renderer", class_="style-scope ytd-item-section-renderer") + +# Retrieve necessary information +df = [] +for t in elem[:100]: # Retrieve only the first 100 video information + title = t.find("yt-formatted-string", class_="style-scope ytd-video-renderer").get_text() + name = t.find("a", class_="yt-simple-endpoint style-scope yt-formatted-string").get_text() + content_url = t.find("a", class_="yt-simple-endpoint style-scope ytd-video-renderer")["href"] + df.append([name, title , 'https://www.youtube.com/'+content_url]) + +## Save data +# Create DataFrame +new = pd.DataFrame(columns=['name', 'title' , 'url_link']) + +# Insert data +for i in range(len(df)): + new.loc[i] = df[i] + +# Create directory to save data +df_dir = "./data/" +if not os.path.exists(df_dir): + os.makedirs(df_dir) + +# Save data +new.to_csv(os.path.join(df_dir, "Youtube_search_df.csv"), index=True, encoding='utf8') # Save with index + +## Save column information +# Column description table +col_names = ['name', 'title' ,'url_link'] +col_exp = ['Channel name', 'Video title', 'URL link'] + +new_exp = pd.DataFrame({'col_names':col_names, + 'col_explanation':col_exp}) + +# Save +new_exp.to_csv(os.path.join(df_dir, "Youtube_col_exp.csv"), index=False, encoding='utf8') + +# Close the browser +browser.close() diff --git a/data/crawling/crawling_videosave.py b/data/crawling/crawling_videosave.py new file mode 100644 index 0000000..8c7c422 --- /dev/null +++ b/data/crawling/crawling_videosave.py @@ -0,0 +1,53 @@ +import os +import pandas as pd +from pytube import YouTube +import time + +''' +Download videos from URLs obtained through 'crawling_urlave.py'. +Videos are saved in the 'download' folder. +''' + +# Read links from the CSV file +csv_file_path = "/Users/imseohyeon/Documents/crawling/data/Youtube_search_df.csv" +df = pd.read_csv(csv_file_path) + +# Define the download folder path +DOWNLOAD_FOLDER = "/Users/imseohyeon/Documents/crawling/download/" + +# Create the download folder if it doesn't exist +if not os.path.exists(DOWNLOAD_FOLDER): + os.makedirs(DOWNLOAD_FOLDER) + +# Iterate over each video and download +for idx, row in df.iterrows(): + video_url = row['url_link'] + try: + # Get video information using Pytube + yt = YouTube(video_url) + length_seconds = yt.length + + # Set the filename + filename = f"{idx}_video.mp4" + + # If the video length exceeds 5 minutes, download only the first 5 minutes + if length_seconds > 5 * 60: + print(f"{yt.title} video exceeds 5 minutes. Downloading only the first 5 minutes.") + stream = yt.streams.filter(adaptive=True, file_extension='mp4').first() + if stream: + print(f"Downloading: {yt.title}") + stream.download(output_path=DOWNLOAD_FOLDER, filename=filename) + print(f"{yt.title} download complete") + else: + print(f"No highest quality stream available for {yt.title}.") + else: + # Download the entire video for videos less than 5 minutes long + stream = yt.streams.get_highest_resolution() + if stream: + print(f"Downloading: {yt.title}") + stream.download(output_path=DOWNLOAD_FOLDER, filename=filename) + print(f"{yt.title} download complete") + else: + print(f"No highest quality stream available for {yt.title}.") + except Exception as e: + print(f"Failed to download {yt.title}: {e}") diff --git a/data/image/image_clipseg2.py b/data/image/image_clipseg2.py new file mode 100644 index 0000000..26f9521 --- /dev/null +++ b/data/image/image_clipseg2.py @@ -0,0 +1,104 @@ +from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation +from PIL import Image +import torch +import numpy as np + +''' +This script performs image segmentation using the CLIPSeg model based on provided text prompts. It loads an image, processes it with text prompts, and generates a segmented image based on the identified objects. + +Inputs: +- image: The image to be segmented. +- positive_prompts: Text prompts describing the objects to be identified, separated by commas. +- negative_prompts: Text prompts describing the objects to be ignored, separated by commas. +- threshold: Threshold value for segmentation, between 0 and 1. + +Outputs: +- output_image: Segmented image with identified objects highlighted. +- final_mask: Final mask representing the segmented areas. + +''' + +# load CLIPSeg model & processor +processor = CLIPSegProcessor.from_pretrained("CIDAS/clipseg-rd64-refined") +model = CLIPSegForImageSegmentation.from_pretrained("CIDAS/clipseg-rd64-refined") + +# image path & load +image_path = "/root/project/voice2face-data/file/face_detected_256x256.png" +image = Image.open(image_path) + +def process_image(image, positive_prompts, negative_prompts, threshold): + ''' + This function performs image segmentation based on provided text prompts and threshold. + + Args: + - image: PIL image object. + - positive_prompts: Text prompts describing the objects to be identified, separated by commas. + - negative_prompts: Text prompts describing the objects to be ignored, separated by commas. + - threshold: Threshold value for segmentation, between 0 and 1. + + Returns: + - output_image: Segmented image with identified objects highlighted. + - final_mask: Final mask representing the segmented areas. + ''' + + # image segmentation with img & prompt + def get_masks(prompts, img, threshold): + prompts = prompts.split(",") + masks = [] + for prompt in prompts: + inputs = processor( + text=prompt.strip(), images=image, padding="max_length", return_tensors="pt" + ) + with torch.no_grad(): + outputs = model(**inputs) + preds = outputs.logits + + pred = torch.sigmoid(preds) + mat = pred.cpu().numpy() + mask = Image.fromarray(np.uint8(mat * 255), "L") + mask = mask.convert("RGB") + mask = mask.resize(image.size) + mask = np.array(mask)[:, :, 0] + + # normalize the mask + mask_min = mask.min() + mask_max = mask.max() + mask = (mask - mask_min) / (mask_max - mask_min) + mask = mask > threshold + masks.append(mask) + return masks + + # Make mask's Positive prompts, Negative prompts + positive_masks = get_masks(positive_prompts, image, threshold) + negative_masks = get_masks(negative_prompts, image, threshold) + + # Make Result mask combined masks + pos_mask = np.any(np.stack(positive_masks), axis=0) + neg_mask = np.any(np.stack(negative_masks), axis=0) + final_mask = pos_mask & ~neg_mask + + # Result image + final_mask = Image.fromarray(final_mask.astype(np.uint8) * 255, "L") + output_image = Image.new("RGBA", image.size, (0, 0, 0, 0)) + output_image.paste(image, mask=final_mask) + return output_image, final_mask + +# base prompt +positive_prompts = "face" +negative_prompts = "background" +threshold = 0.5 + +# 텍스트 프롬프트 및 임계값 설정 +# positive_prompts = input("what you want to identify (comma separated): ") +# negative_prompts = input("what you want to ignore (comma separated): ") +# threshold = float(input("enter the threshold value (between 0 and 1): ")) + +# process of segmentation +output_image, final_mask = process_image(image, positive_prompts, negative_prompts, threshold) + +# save result img +output_image_path = "/root/project/voice2face-data/file/segmented_image.png" +output_image.save(output_image_path) + +# print success message +print("Segmented image saved successfully at:", output_image_path) diff --git a/data/image/image_face_frame.py b/data/image/image_face_frame.py new file mode 100644 index 0000000..56bb442 --- /dev/null +++ b/data/image/image_face_frame.py @@ -0,0 +1,78 @@ +import cv2 +import os +from rembg import remove + +def process_video(video_path: str, save_folder: str): + """Video 에서 사람의 얼굴이 정면이고, 눈을 뜨고 잇을 때 캡쳐 및 배경 제거 + + Args: + video_path (str): 타겟 비디오 파일경로 + save_folder (str): 이미지를 저장할 위치 + """ + face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_alt2.xml') + eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye_tree_eyeglasses.xml') + + cap = cv2.VideoCapture(video_path) + + while True: + ret, frame = cap.read() + if not ret: + break + + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + faces = face_cascade.detectMultiScale(gray, 1.1, 4) + + for (x, y, w, h) in faces: + # 얼굴 영역을 30% 확장 + expansion_rate = 0.3 + new_width = int(w * (1 + expansion_rate)) + new_height = int(h * (1 + expansion_rate)) + new_x = x - int(w * expansion_rate / 2) + new_y = y - int(h * expansion_rate / 2) + + # 확장된 영역이 프레임을 벗어나지 않도록 조정 + new_x = max(0, new_x) + new_y = max(0, new_y) + new_width = min(new_width, frame.shape[1] - new_x) + new_height = min(new_height, frame.shape[0] - new_y) + + roi_color = frame[new_y:new_y+new_height, new_x:new_x+new_width] + eyes = eye_cascade.detectMultiScale(cv2.cvtColor(roi_color, cv2.COLOR_BGR2GRAY)) + if len(eyes) >= 2: # 두 눈이 검출되면 + face_img = cv2.resize(roi_color, (256, 256)) + face_img = remove(face_img, bgcolor=(255, 255, 255, 255)) + + video_name = "_".join(os.path.splitext(os.path.basename(video_path))[0].split("_")[:-2]) + origin_file_folder = os.path.join(save_folder,'data', video_name) + os.makedirs(origin_file_folder, exist_ok=True) + + cv2.imwrite(f'{os.path.join(origin_file_folder, video_name)}.png', face_img) + cap.release() + return + + print(os.path.basename(video_path)) + cap.release() + + +def main(video_folder: str, save_folder: str): + """ 비디오 폴더 경로에서 .mp4 파일 선택, + 정제 이후 파일 제거 + + Args: + video_folder (str): 탐색할 비디오가 있는 경로 + save_folder (str): 이미지 저장을 위한 파일 + """ + for root,_,files in os.walk(video_folder): + for file in files: + if file.endswith('.mp4'): + video_file = os.path.join(root, file) + process_video(video_file, save_folder) + os.remove(os.path.join(root,file)) + + +if __name__ == '__main__': + video_folder = '/home/carbox/Desktop/data/009.립리딩(입모양) 음성인식 데이터/01.데이터/2.Validation/원천데이터' + save_folder = "/home/carbox" + main(video_folder, save_folder) + + diff --git a/data/relabel/relabel_Vox_age.py b/data/relabel/relabel_Vox_age.py new file mode 100644 index 0000000..842afa7 --- /dev/null +++ b/data/relabel/relabel_Vox_age.py @@ -0,0 +1,84 @@ +import cv2 +import argparse +import os +import csv +from collections import Counter + +def predict_age(face): + ''' + Function to predict age from a face image. + + Args: + face: Image to predict age from. + + Returns: + age: Predicted age group index. + ''' + blob = cv2.dnn.blobFromImage(face, 1.0, (227, 227), MODEL_MEAN_VALUES, swapRB=False) + ageNet.setInput(blob) + agePreds = ageNet.forward() + age = agePreds[0].argmax() + return age + +def count_and_print_age(folder_path): + ''' + Function to count and print the most common age group from images in a folder. + + Args: + folder_path: Path to the folder containing images. + + Returns: + most_common_age: Index of the most common age group. + ''' + age_list = [] + try: + for filename in os.listdir(folder_path): + if filename.endswith(".jpg"): + image_path = os.path.join(folder_path, filename) + frame = cv2.imread(image_path) + if frame is not None: + age = predict_age(frame) + age_list.append(age) + except FileNotFoundError: + print(f"Folder '{folder_path}' not found.") + return None + + if age_list: + most_common_age = Counter(age_list).most_common(1)[0][0] + print("Most common age group index:", most_common_age) + return most_common_age + else: + print("No images detected in the folder.") + return None + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--folder', required=True, help='Path to the folder containing images.') + parser.add_argument('--csv_source', default="/home/carbox/Desktop/git/dataset/vox1/vox1_meta.csv") + parser.add_argument('--output_csv', default="/home/carbox/Desktop/git/dataset/test/csv/test.csv") + args = parser.parse_args() + + ageProto = "weights/age_deploy.prototxt" + ageModel = "weights/age_net.caffemodel" + + MODEL_MEAN_VALUES = (78.4263377603, 87.7689143744, 114.895847746) + ageList = ['0', '1', '2', '3', '4', '5', '6', '7'] # Index 형태로 출력하기 위해 수정 + + ageNet = cv2.dnn.readNet(ageModel, ageProto) + + with open (args.output_csv, 'w', newline='') as output_csv: + csvwriter = csv.writer(output_csv, delimiter='\t') + + with open(args.csv_source, 'r') as file: + for idx, line in enumerate(file): + if idx == 0: + csvwriter.writerow(line.strip().split("\t") + ["age"]) # 헤더 부분에 age를 추가하여 리스트를 연결 + continue + line = line.strip() # Remove leading/trailing whitespaces + if line: + fields = line.split("\t") + image_name = fields[1] + age_index = count_and_print_age(os.path.join(args.folder, image_name)) + if age_index is not None: + fields.append(str(age_index)) # age_index를 문자열로 변환하여 fields에 추가 + csvwriter.writerow(fields) diff --git a/data/relabel/relabel_detect_getframe.py b/data/relabel/relabel_detect_getframe.py new file mode 100644 index 0000000..73915b4 --- /dev/null +++ b/data/relabel/relabel_detect_getframe.py @@ -0,0 +1,141 @@ +import cv2 +import math +import argparse +import os + +def highlightFace(net, frame, conf_threshold=0.7): + ''' + Function to detect faces in a frame using a pre-trained deep learning model. + + Args: + net: Pre-trained deep learning model. + frame: Input frame. + conf_threshold: Confidence threshold for face detection. + + Returns: + frameOpencvDnn: Copy of the input frame with face rectangles drawn. + faceInfo: List containing information about detected faces (bbox, center, width, height). + ''' + frameOpencvDnn = frame.copy() + frameHeight = frameOpencvDnn.shape[0] + frameWidth = frameOpencvDnn.shape[1] + blob = cv2.dnn.blobFromImage(frameOpencvDnn, 1.0, (300, 300), [104, 117, 123], True, False) + + net.setInput(blob) + detections = net.forward() + faceInfo = [] + for i in range(detections.shape[2]): + confidence = detections[0, 0, i, 2] + if confidence > conf_threshold: + x1 = int(detections[0, 0, i, 3] * frameWidth) + y1 = int(detections[0, 0, i, 4] * frameHeight) + x2 = int(detections[0, 0, i, 5] * frameWidth) + y2 = int(detections[0, 0, i, 6] * frameHeight) + cx = (x1 + x2) // 2 + cy = (y1 + y2) // 2 + w = x2 - x1 + h = y2 - y1 + faceInfo.append({'bbox': (x1, y1, x2, y2), 'center': (cx, cy), 'width': w, 'height': h}) + cv2.rectangle(frameOpencvDnn, (x1, y1), (x2, y2), (0, 255, 0), int(round(frameHeight / 150)), 8) + return frameOpencvDnn, faceInfo + +def save_frame(frame, output_folder, frame_count, folder_name, gender, age, center): + ''' + Function to save a frame with a specific filename format. + + Args: + frame: Frame to be saved. + output_folder: Folder where frames will be saved. + frame_count: Frame count. + folder_name: Name of the folder containing the video. + gender: Gender of the detected face. + age: Age range of the detected face. + center: Center coordinates of the detected face bbox. + ''' + if not os.path.exists(output_folder): + os.makedirs(output_folder) + subfolder_path = os.path.join(output_folder, folder_name) + if not os.path.exists(subfolder_path): + os.makedirs(subfolder_path) + cv2.imwrite(os.path.join(subfolder_path, f"{gender}_{age}_{center[0]}-{center[1]}.jpg"), frame) + +parser = argparse.ArgumentParser() +parser.add_argument('--folder', default='/Users/imseohyeon/Documents/gad/video') +parser.add_argument('--output_folder', default='frame') +parser.add_argument('--capture_interval', type=int, default=50) # Adjusted frame interval for capturing + +args = parser.parse_args() + +faceProto = "opencv_face_detector.pbtxt" +faceModel = "opencv_face_detector_uint8.pb" +ageProto = "age_deploy.prototxt" +ageModel = "age_net.caffemodel" +genderProto = "gender_deploy.prototxt" +genderModel = "gender_net.caffemodel" + +MODEL_MEAN_VALUES = (78.4263377603, 87.7689143744, 114.895847746) +ageList = ['(0-2)', '(4-6)', '(8-12)', '(15-20)', '(25-32)', '(38-43)', '(48-53)', '(60-100)'] +genderList = ['Male', 'Female'] + +faceNet = cv2.dnn.readNet(faceModel, faceProto) +ageNet = cv2.dnn.readNet(ageModel, ageProto) +genderNet = cv2.dnn.readNet(genderModel, genderProto) + +for root, dirs, files in os.walk(args.folder): + for folder_name in dirs: + folder_path = os.path.join(root, folder_name) + for filename in os.listdir(folder_path): + if filename.endswith(".mp4"): + video_path = os.path.join(folder_path, filename) + break + else: + continue + + video = cv2.VideoCapture(video_path) + padding = 20 + frame_count = 0 + while True: # Infinite loop for processing each frame + hasFrame, frame = video.read() + if not hasFrame: + break + + resultImg, faceInfo = highlightFace(faceNet, frame) + if faceInfo: # Process only if faces are detected + for faceData in faceInfo: + bbox = faceData['bbox'] + center = faceData['center'] + width = faceData['width'] + height = faceData['height'] + + face = frame[max(0, bbox[1] - padding): min(bbox[3] + padding, frame.shape[0] - 1), + max(0, bbox[0] - padding): min(bbox[2] + padding, frame.shape[1] - 1)] + + blob = cv2.dnn.blobFromImage(face, 1.0, (227, 227), MODEL_MEAN_VALUES, swapRB=False) + genderNet.setInput(blob) + genderPreds = genderNet.forward() + gender = genderList[genderPreds[0].argmax()] + print(f'Gender: {gender}') + + ageNet.setInput(blob) + agePreds = ageNet.forward() + age = ageList[agePreds[0].argmax()] + print(f'Age: {age[1:-1]} years') + + cv2.putText(resultImg, f'{gender}, {age}', (bbox[0], bbox[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2, cv2.LINE_AA) + cv2.putText(resultImg, f'Center: ({center[0]}, {center[1]})', (bbox[0], bbox[1] - 40), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2, cv2.LINE_AA) + cv2.putText(resultImg, f'Box: ({bbox[0]}, {bbox[1]}, {width}, {height})', (bbox[0], bbox[1] - 70), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2, cv2.LINE_AA) + + cv2.imshow("Detecting age and gender", resultImg) + + # Capture and save frames at specified intervals + if frame_count % args.capture_interval == 0: + save_frame(frame, args.output_folder, frame_count, folder_name, gender, age, center) + + frame_count += 1 + + # Press 'q' to exit + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + video.release() + cv2.destroyAllWindows() diff --git a/data/relabel/relabel_select_csv.py b/data/relabel/relabel_select_csv.py new file mode 100644 index 0000000..0ece279 --- /dev/null +++ b/data/relabel/relabel_select_csv.py @@ -0,0 +1,57 @@ +import os +import pandas as pd +from argparse import ArgumentParser + +def parse_args(): + parser = ArgumentParser() + + # Conventional args + parser.add_argument('--csv_file', type=str, default='output_test.csv') + parser.add_argument('--data_path', type=str, default='origin/video') + parser.add_argument('--save_csv', type=str, default='new_output.csv') + + args = parser.parse_args() + + return args + + +def list_files_and_folders(data_path): + if os.path.isdir(data_path): + items = os.listdir(data_path) + return items + else: + return None + +def main(csv_file, data_path, save_csv): + ageList_s = [0,4,8,15,25,38,48,60] + + csv_data = pd.read_csv(csv_file, header=None) + youtube_ids = list_files_and_folders(data_path) + count = 0 + for youtube_id in youtube_ids: + filtered_df = csv_data[csv_data[0].astype(str).str.contains(youtube_id)] + first_row = filtered_df.iloc[0:1] + file_name = list_files_and_folders(os.path.join(data_path, youtube_id)) + for i in file_name: + file_name_list = i.split("_") + if file_name_list[-1] =="Store": + continue + else: + print(youtube_id, file_name_list) + + first_row[4] = file_name_list[0] + + first_row[5] = ageList_s.index(int(file_name_list[1])) + + first_row.to_csv(save_csv, mode="a", index=False, header=False) + + + count += 1 + + + print(count) + + +if __name__ == '__main__': + args = parse_args() + main(**args.__dict__) \ No newline at end of file diff --git a/data/requirements.txt b/data/requirements.txt new file mode 100644 index 0000000..6a236ce --- /dev/null +++ b/data/requirements.txt @@ -0,0 +1,18 @@ +opencv-python +rembg +librosa +numpy +matplotlib +soundfile +pydub +pandas +moviepy +face_recognition +selenium +requests +beautifulsoup4 +pytube +transformers +Pillow +torch +facenet-pytorch diff --git a/data/total/total_audio_video_image.py b/data/total/total_audio_video_image.py new file mode 100644 index 0000000..0b6384a --- /dev/null +++ b/data/total/total_audio_video_image.py @@ -0,0 +1,102 @@ +import os +import numpy as np +import librosa +import soundfile as sf +import matplotlib.pyplot as plt +from pydub import AudioSegment +import cv2 +from facenet_pytorch import MTCNN +import subprocess + +# Initialize MTCNN for face detection +mtcnn = MTCNN() + +from moviepy.editor import VideoFileClip + +# 1. 비디오에서 음성 추출 +def extract_audio_from_video(video_file, audio_file): + # ffmpeg를 사용하여 비디오에서 오디오 추출 + command = f"ffmpeg -i {video_file} -vn -acodec pcm_s16le -ar 44100 -ac 2 {audio_file}" + subprocess.call(command, shell=True) + +# 2. 사람 음성부분 추출 +def detect_human_voice(audio_file): + # 오디오 파일에서 사람 음성부분을 감지하여 해당 인덱스 반환 + y, sr = librosa.load(audio_file, sr=None) + voice_segments = librosa.effects.split(y, top_db=18) + voice_indices = [] + for start, end in voice_segments: + if end - start >= sr * 1: # 1초 이상인 경우에만 추가 + voice_indices.extend(range(start, end)) + return voice_indices + +# 3. 음성부분만 모아 다시 저장. + 비디오도 이 간격 맞춰 다시 저장 +def save_detected_voice(audio_file, video_file, save_audio_file, save_video_file): + # 감지된 사람 음성부분을 추출하여 저장 + y, sr = librosa.load(audio_file, sr=None) + voice_indices = detect_human_voice(audio_file) + combined_audio = y[voice_indices] + sf.write(save_audio_file, combined_audio, sr) + + # 비디오도 해당 음성에 맞게 잘라서 저장 + audio_clip = AudioSegment.from_wav(save_audio_file) + video_clip = VideoFileClip(video_file) + video_duration = int(video_clip.duration * 1000) # 비디오의 길이를 정수로 변환 + if len(audio_clip) > video_duration: + audio_clip = audio_clip[:video_duration] + else: + audio_clip += audio_clip[-1] * (video_duration - len(audio_clip)) + + audio_clip.export(save_audio_file, format="wav") + video_clip.write_videofile(save_video_file, codec='libx264', audio_codec='aac') + +# 4. 새로운 비디오에서 얼굴인식되는 부분중에 frame 추출 + frame에서도 256x256으로 얼굴 부분 bbox 맞춰 잘라 이미지 저장. +def extract_frames_with_faces(video_file, output_folder): + # 비디오 파일에서 프레임 추출하여 얼굴을 인식하고 이미지 저장 + cap = cv2.VideoCapture(video_file) + frame_rate = cap.get(cv2.CAP_PROP_FPS) + frame_count = 0 + success, frame = cap.read() + while success: + frame_count += 1 + if frame_count % (10 * frame_rate) == 0: # 10초마다 프레임 추출 + try: + boxes, _ = mtcnn.detect(frame) + if boxes is not None: + for i, box in enumerate(boxes): + x, y, w, h = [int(coord) for coord in box] + face_image = frame[y:y+h, x:x+w] + cv2.imwrite(os.path.join(output_folder, f"frame_{frame_count}_{i}.jpg"), face_image) + except Exception as e: + print(f"Failed to detect face in frame {frame_count}: {e}") + success, frame = cap.read() + cap.release() + + +# Define paths +video_file_path = "/Users/imseohyeon/Documents/voice2face-data/code/file/testvideo.mp4" +audio_file_path = "/Users/imseohyeon/Documents/voice2face-data/code/file/testaudio.mp3" +detected_voice_file_path = "/Users/imseohyeon/Documents/voice2face-data/code/file/combined_voice.wav" +output_frame_folder = "/Users/imseohyeon/Documents/voice2face-data/code/file/images" +trimmed_video_file_path = "/Users/imseohyeon/Documents/voice2face-data/code/file/trimmed_video.mp4" + +# Convert audio file to WAV format +converted_audio_file_path = os.path.splitext(audio_file_path)[0] + ".wav" +AudioSegment.from_file(audio_file_path).export(converted_audio_file_path, format="wav") + +# Extract audio from video +extract_audio_from_video(video_file_path, converted_audio_file_path) + +# Create necessary folders +for folder in [output_frame_folder, os.path.dirname(detected_voice_file_path)]: + os.makedirs(folder, exist_ok=True) + +# Step 2: Save the detected human voice segment and corresponding video +save_detected_voice(converted_audio_file_path, video_file_path, detected_voice_file_path, trimmed_video_file_path) + +# If no human voice segments are detected, delete the corresponding video file +if not os.path.exists(detected_voice_file_path): + os.remove(trimmed_video_file_path) +else: + # Step 3: Extract frames with detected faces every 10 seconds + extract_frames_with_faces(trimmed_video_file_path, output_frame_folder) diff --git a/data/total/total_origin_remove.py b/data/total/total_origin_remove.py new file mode 100644 index 0000000..68f66a0 --- /dev/null +++ b/data/total/total_origin_remove.py @@ -0,0 +1,60 @@ +import os +import re +import tarfile + +def extract_tar_if_needed(root_folder: str): + """ tar 파일을 추출하는 함수 + + Args: + root_folder (str): tar 파일이 들어있는 폴더 경로 + """ + for root, dirs, files in os.walk(root_folder): + for file in files: + if file.endswith('.tar'): + # .tar 파일과 같은 이름을 가진 폴더가 있는지 확인 + tar_path = os.path.join(root, file) + folder_name = os.path.splitext(tar_path)[0] + if not os.path.exists(folder_name): + # 동일한 이름의 폴더가 없으면 압축 해제 + print(f"Extracting {tar_path}") + with tarfile.open(tar_path) as tar: + tar.extractall(path=root) + print(f"Extracted to {folder_name}") + # 압축 해제 후 dirs 리스트 업데이트 + dirs.append(os.path.basename(folder_name)) + + # 추출 후 삭제 + os.remove(os.path.join(root, file)) + print(f"Deleted {file} ") + + + +def delete_files_except_extensions(root_folder: str, keep_extensions: list, pattern: str): + """ 정제에 필요한 파일을 제외한 다른 파일들 삭제 + + Args: + root_folder (str): 타겟 폴더 + keep_extensions (list): 유지할 확장자 wav, 혹은 끝나는 부분 (A_001.mp4) 확인 + pattern (str): 유지할 파일명 패턴 + """ + for root, dirs, files in os.walk(root_folder): + for file in files: + if not any(file.endswith(ext) for ext in keep_extensions): + if re.search(pattern, file): + continue + file_path = os.path.join(root, file) + os.remove(file_path) + print(f"Deleted {file}") + +def main(root_folder: str, keep_extensions: list, pattern: str): + # 먼저 .tar 파일이 있으면 압축을 해제 + extract_tar_if_needed(root_folder) + # 압축 해제 후 (해당되는 경우), 지정된 확장자 외의 파일 삭제 + delete_files_except_extensions(root_folder, keep_extensions, pattern) + print("End of processing") + +if __name__ == '__main__': + root_folder = '~/Desktop/GPU/009.립리딩(입모양) 음성인식 데이터/01.데이터/2.Validation/원천데이터' # 탐색을 시작할 폴더 경로 + keep_extensions = ['A_001.mp4', '.wav'] # 유지하고 싶은 파일 확장자 목록 + pattern = r'.*_A_.*\.json' + main(root_folder, keep_extensions, pattern) diff --git a/data/video/video_clipimage.py b/data/video/video_clipimage.py new file mode 100644 index 0000000..e5c1bfb --- /dev/null +++ b/data/video/video_clipimage.py @@ -0,0 +1,41 @@ +import cv2 + +def detect_and_save_faces(image_path, output_path): + """Detects faces in the input image and saves them as 256x256 pixel images. + + Args: + image_path (str): Path to the input image file. + output_path (str): Path to save the detected face images. + """ + # Load the pre-trained face cascade classifier + face_cascade = cv2.CascadeClassifier('code/file/haarcascade_frontalface_default.xml') + + # Read the input image + img = cv2.imread(image_path) + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + + # Detect faces + faces = face_cascade.detectMultiScale(gray, 1.3, 5) + + # Iterate over each detected face + for (x, y, w, h) in faces: + # Draw rectangle around the face + cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 2) + + # Resize the face region to 256x256 pixels + face_crop = cv2.resize(img[y:y+h, x:x+w], (256, 256)) + + # Save the face image + cv2.imwrite(output_path, face_crop) + + # Display the image + cv2.imshow('Image view', img) + + # Wait for 'q' key to be pressed + while cv2.waitKey(0) & 0xFF != ord('q'): + pass + + cv2.destroyAllWindows() + +# Example usage +detect_and_save_faces('code/file/image.png', 'code/file/face_detected_256x256.png') diff --git a/data/video/video_download.py b/data/video/video_download.py new file mode 100644 index 0000000..5b156a0 --- /dev/null +++ b/data/video/video_download.py @@ -0,0 +1,99 @@ +from pytube import YouTube # 유튜브 영상을 다운로드하기 위한 모듈 +import os.path # 경로를 설정하기 위한 모듈 +import ffmpeg # 미디어를 변환하기 위한 모듈 +from getpass import getuser # 기본 경로를 다운로드 폴더로 지정하기 위한 모듈 + +class Download: + ''' + 파일을 변환하기 위해선 ffmpeg란 프로그램을 별도로 설치해 컴퓨터 환경변수 설정을 마쳐야 함. + ''' + + def __init__(self, link): + ''' + Download 클래스의 생성자입니다. + + Args: + link (str): 유튜브 영상의 링크. + ''' + # link 인자는 GUI에서 입력된 값을 받을 때 사용 + # 컴퓨터 이용자명을 받아서 다운로드 폴더를 기본 폴더로 지정 + self.parent_dir = f"/Users/{getuser()}/Documents/voice2face-data/code/file" + self.yt = YouTube(link) + + def getVideoName(self): + '''(GUI 버전) 비디오 이름을 내보내는 함수''' + ''' + 유튜브 비디오의 제목을 반환하는 함수입니다. + + Returns: + str: 비디오의 제목. + ''' + name = self.yt.title + return name + + def downloadMp3(self): + '''mp3 파일로 다운로드하는 함수''' + ''' + mp3 형식으로 비디오를 다운로드하는 함수입니다. + + Returns: + str: 다운로드한 mp3 파일의 이름. + ''' + # mp4 형태지만 영상 없이 소리만 있는 파일 다운로드 + stream = self.yt.streams.filter(only_audio=True).first() + stream.download(self.parent_dir) + + src = stream.default_filename # mp4로 다운받은 영상 제목(파일명과 같음) + dst = "testaudio.mp3" # 변경된 파일명 + + # mp4에서 mp3로 변환 + ffmpeg.input(os.path.join(self.parent_dir, src)).output(os.path.join(self.parent_dir, dst)).run(overwrite_output=True) + + # 변환되기 전 mp4 파일 삭제 + os.remove(os.path.join(self.parent_dir, src)) + + return dst # 저장한 파일명 리턴 + + def downloadMp4(self): + '''mp4 파일로 다운로드하는 함수''' + ''' + mp4 형식으로 비디오를 다운로드하는 함수입니다. + + Returns: + str: 다운로드한 mp4 파일의 이름. + ''' + audio = self.downloadMp3() # mp3 파일 다운로드 + video = self.yt.streams.filter(adaptive=True, file_extension='mp4').first() # 비디오 객체 가져오기 + print(video) + video.download(self.parent_dir) # mp4 파일 다운로드 + + # mp4로 해상도 높은 파일을 받으면 vcodec만 존재 + # -> 비디오에 소리를 입히려면 acodec 있는 파일 받아 FFmpeg로 병합 + # -> downloadMp3로 mp3 파일을 받고 오디오 소스로 사용 + inputAudio = ffmpeg.input(os.path.join(self.parent_dir, audio)) + inputVideo = ffmpeg.input(os.path.join(self.parent_dir, video.default_filename)) + + # 영상에 소리 입혀 "new.mp4"파일로 내보내기 + ffmpeg.output(inputAudio, inputVideo, os.path.join(self.parent_dir, "new.mp4"), vcodec='copy', acodec='aac').run(overwrite_output=True) + + # # 변환이 끝나 더 이상 필요 없는 mp3, mp4 파일 지우기 + # os.remove(os.path.join(self.parent_dir, video.default_filename)) + # os.remove(os.path.join(self.parent_dir, audio)) + + # "new.mp4"를 영상 제목으로 바꾸기 + os.rename(os.path.join(self.parent_dir, "new.mp4"), os.path.join(self.parent_dir, video.default_filename)) + + return video.default_filename # 저장한 파일명 리턴 + +# 위의 클래스 정의 부분을 여기에 넣어주세요. + +# 다운로드를 수행할 링크 +link = "https://youtu.be/2DnGKEeRB4g?si=93Cf_Mg2n53kSpGQ" + +# Download 클래스 인스턴스 생성 +downloader = Download(link) + +# mp4 파일로 다운로드 +downloaded_file = downloader.downloadMp4() + +print(f"다운로드가 완료되었습니다: {downloaded_file}") diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..8f6c617 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +.vercel +.output +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..5935de9 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:18 as build + +WORKDIR /app + +COPY package.json ./ +COPY package-lock.json ./ +RUN npm install +RUN npm install -g pm2 + +COPY . ./ +RUN npm run build + +#CMD ["pm2-runtime", "start", "./build/index.js", "--env", "production"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..497f1ad --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,157 @@ +# 🔊목소리로 가상 얼굴 생성 서비스 [너의 목소리가 보여] + + + + + +## Project Structure + +``` +frontend +┣ src +┃ ┣ components +┃ ┃ ┣ button +┃ ┃ ┃ ┣ basic_filled.svelte +┃ ┃ ┃ ┣ moving_filled.svelte +┃ ┃ ┃ ┣ request_filled.svelte +┃ ┃ ┃ ┗ result_save.svelte +┃ ┃ ┣ header +┃ ┃ ┃ ┣ header_login.svelte +┃ ┃ ┃ ┗ header_non.svelte +┃ ┃ ┣ image +┃ ┃ ┃ ┣ PlaceholderImage.svelte +┃ ┃ ┃ ┗ SocialProperty1Github.svelte +┃ ┃ ┣ modal +┃ ┃ ┃ ┗ basic_modal.svelte +┃ ┃ ┣ rating +┃ ┃ ┃ ┣ Star.svelte +┃ ┃ ┃ ┗ StarRating.svelte +┃ ┃ ┗ survey +┃ ┃ ┣ input_area.svelte +┃ ┃ ┗ survey.svelte +┃ ┣ routes +┃ ┃ ┣ aboutus +┃ ┃ ┃ ┗ +page.svelte +┃ ┃ ┣ home +┃ ┃ ┃ ┗ +page.svelte +┃ ┃ ┣ infogather +┃ ┃ ┃ ┣ +page.svelte +┃ ┃ ┃ ┗ VoiceButtonDefaultVariant3.svelte +┃ ┃ ┣ join +┃ ┃ ┃ ┣ +page.svelte +┃ ┃ ┃ ┣ ButtonStyleFilled.svelte +┃ ┃ ┃ ┣ ButtonStyleOutlined.svelte +┃ ┃ ┃ ┣ PlaceholderImage.svelte +┃ ┃ ┃ ┣ radio.svelte +┃ ┃ ┃ ┣ RadioBigDefault.svelte +┃ ┃ ┃ ┗ RadioSmallS.svelte +┃ ┃ ┣ loading +┃ ┃ ┃ ┗ +page.svelte +┃ ┃ ┣ login +┃ ┃ ┃ ┣ +page.svelte +┃ ┃ ┃ ┗ PlaceholderImage.svelte +┃ ┃ ┣ maintenance +┃ ┃ ┃ ┗ +page.svelte +┃ ┃ ┣ result +┃ ┃ ┃ ┣ +page.svelte +┃ ┃ ┃ ┗ PlaceholderImage.svelte +┃ ┃ ┗ resultlist +┃ ┃ ┃ ┣ +page.svelte +┃ ┃ ┃ ┗ ButtonStyleFilled.svelte +┃ ┃ ┣ +layout.svelte +┃ ┃ ┣ +page.js +┃ ┃ ┣ +page.svelte +┃ ┃ ┣ styles.css +┃ ┣ app.html +┃ ┣ app.pcss +┃ ┗ hooks.js +┣ static +┣ .gitignore +┣ .npmrc +┣ Dockerfile +┣ package.json +┣ package-lock.json +┣ postcss.config.cjs +┣ README.md +┣ svelte.config.js +┣ tailwind.config.cjs +┗ vite.config.js +``` +## Usage + + +### `src` +#### `components` +: 페이지 내부에 포함할 컴포넌트를 모듈화 하기 위해 보관해둔 폴더 + - `button`: 화면 변경이나, input event 등의 trigger로 사용하기 위한 버튼을 보관한 폴더 + - `header`: 웹사이트 header 보관 폴더 + - `image`: 웹에 사용되는 이미지 style을 지정할 폴더 + - `modal`: 주의사항을 표시할 modal을 보관한 폴더 + - `rating`: 별점 표시, 전송을 위한 컴포넌트를 보관한 폴더 + - `survey`: 설문조사 항목에 대한 컴포넌트를 보관한 폴더 + +#### `routes` +: 서비스에 사용된 페이지를 담아둔 폴더 + + - `aboutus`: 팀, 팀원 소개를 위한 페이지 + - `home`: 로그인 후 생성요청이나, 결과 확인으로 이동할 수 있는 화면 + - `infogather`: 사용자가 생성요청을 보낼 수 있는 페이지 + - `VoiceButtonDefaultVariant3.svelte`: 음성 녹음 및 Blob을 생성하기 위한 모듈 + - `join`: 회원가입을 위한 페이지 + - `loading`: 사용자가 이미지 생성 요청 후 넘어갈 수 있는 페이지로 결과나 `home`화면으로 이동할 수 있다. + - `login`: 로그인 페이지 + - `maintenance`: 서비스 점검 시 사용될 페이지 + - `result`: 결과를 확인할 수 있는 페이지 + - `resultlist`: 결과 목록 페이지 + +#### `hooks.js` +: 서비스 점검 시 모든 화면을 maintenance 화면으로 이동하는 event 를 발생시킬 훅을 추가하는 파일 +### `static` +: 서비스에 사용된 이미지를 보관해둔 폴더 + + +## Getting Started +This project was developed and tested on the following operating systems: +- **Linux**: Ubuntu 20.04 LTS +- **Windows**: Windows 11 Home + +### 1. Install Requirements + +To run this project, you'll need Node.js installed on your system. We recommend using NVM (Node Version Manager) to manage your Node.js versions. +``` +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash +source ~/.bashrc +nvm install 20.11.1 +``` + +This project uses Tailwind CSS, Flowbite, and Svelte routing for UI and navigation. +``` +# basic package +npm install +npx svelte-add@latest tailwindcss +npm install -D tailwindcss svelte-routing flowbite-svelte flowbite +``` +### 2. Developing + +To start the development server, run: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +### 3. Building + +To create a production build, use: + +``` +npm run build +``` + +## Links +- [Wireframe](https://www.figma.com/file/MBWE1CthewJVCl0KH8xEcM/%5BV1%5D-%ED%99%94%EB%A9%B4-%EA%B5%AC%EC%84%B1?type=whiteboard&node-id=0:1&t=jPWmEIfP3RobQG6G-1) +- [Prototyping](https://www.figma.com/file/fN6DWRmoszsytULLaZ4cni/Voice2Face-V1?type=design&node-id=1603:2&mode=design&t=jPWmEIfP3RobQG6G-1) +- [Origin github](https://github.com/Make-Zenerator/voice2face-frontend.git) + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..6ec85e6 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4460 @@ +{ + "name": "mz-v1-front", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mz-v1-front", + "version": "0.0.1", + "dependencies": { + "pm2": "^5.3.1" + }, + "devDependencies": { + "@fontsource/fira-mono": "^4.5.10", + "@neoconfetti/svelte": "^1.0.0", + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/adapter-node": "^5.0.1", + "@sveltejs/kit": "^2.5.2", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "autoprefixer": "^10.4.16", + "flowbite": "^2.3.0", + "flowbite-svelte": "^0.44.24", + "postcss": "^8.4.32", + "postcss-load-config": "^5.0.2", + "svelte": "^4.2.7", + "tailwindcss": "^3.3.6", + "vite": "^5.0.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dev": true, + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dev": true, + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==", + "dev": true + }, + "node_modules/@fontsource/fira-mono": { + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-4.5.10.tgz", + "integrity": "sha512-bxUnRP8xptGRo8YXeY073DSpfK74XpSb0ZyRNpHV9WvLnJ7TwPOjZll8hTMin7zLC6iOp59pDZ8EQDj1gzgAQQ==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz", + "integrity": "sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz", + "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@neoconfetti/svelte": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@neoconfetti/svelte/-/svelte-1.0.0.tgz", + "integrity": "sha512-SmksyaJAdSlMa9cTidVSIqYo1qti+WTsviNDwgjNVm+KQ3DRP2Df9umDIzC4vCcpEYY+chQe0i2IKnLw03AT8Q==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opencensus/core": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.9.tgz", + "integrity": "sha512-31Q4VWtbzXpVUd2m9JS6HEaPjlKvNMOiF7lWKNmXF84yUcgfAFL5re7/hjDmdyQbOp32oGc+RFV78jXIldVz6Q==", + "dependencies": { + "continuation-local-storage": "^3.2.1", + "log-driver": "^1.2.7", + "semver": "^5.5.0", + "shimmer": "^1.2.0", + "uuid": "^3.2.1" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@opencensus/core/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@opencensus/propagation-b3": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@opencensus/propagation-b3/-/propagation-b3-0.0.8.tgz", + "integrity": "sha512-PffXX2AL8Sh0VHQ52jJC4u3T0H6wDK6N/4bg7xh4ngMYOIi13aR1kzVvX1sVDBgfGwDOkMbl4c54Xm3tlPx/+A==", + "dependencies": { + "@opencensus/core": "^0.0.8", + "uuid": "^3.2.1" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@opencensus/propagation-b3/node_modules/@opencensus/core": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.8.tgz", + "integrity": "sha512-yUFT59SFhGMYQgX0PhoTR0LBff2BEhPrD9io1jWfF/VDbakRfs6Pq60rjv0Z7iaTav5gQlttJCX2+VPxFWCuoQ==", + "dependencies": { + "continuation-local-storage": "^3.2.1", + "log-driver": "^1.2.7", + "semver": "^5.5.0", + "shimmer": "^1.2.0", + "uuid": "^3.2.1" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@opencensus/propagation-b3/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pm2/agent": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.3.tgz", + "integrity": "sha512-xkqqCoTf5VsciMqN0vb9jthW7olVAi4KRFNddCc7ZkeJZ3i8QwZANr4NSH2H5DvseRFHq7MiPspRY/EWAFWWTg==", + "dependencies": { + "async": "~3.2.0", + "chalk": "~3.0.0", + "dayjs": "~1.8.24", + "debug": "~4.3.1", + "eventemitter2": "~5.0.1", + "fast-json-patch": "^3.0.0-1", + "fclone": "~1.0.11", + "nssocket": "0.6.0", + "pm2-axon": "~4.0.1", + "pm2-axon-rpc": "~0.7.0", + "proxy-agent": "~6.3.0", + "semver": "~7.5.0", + "ws": "~7.4.0" + } + }, + "node_modules/@pm2/agent/node_modules/dayjs": { + "version": "1.8.36", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.36.tgz", + "integrity": "sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==" + }, + "node_modules/@pm2/agent/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/agent/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/io": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@pm2/io/-/io-5.0.2.tgz", + "integrity": "sha512-XAvrNoQPKOyO/jJyCu8jPhLzlyp35MEf7w/carHXmWKddPzeNOFSEpSEqMzPDawsvpxbE+i918cNN+MwgVsStA==", + "dependencies": { + "@opencensus/core": "0.0.9", + "@opencensus/propagation-b3": "0.0.8", + "async": "~2.6.1", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "require-in-the-middle": "^5.0.0", + "semver": "~7.5.4", + "shimmer": "^1.2.0", + "signal-exit": "^3.0.3", + "tslib": "1.9.3" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@pm2/io/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@pm2/io/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==" + }, + "node_modules/@pm2/io/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/io/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/io/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/@pm2/js-api": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@pm2/js-api/-/js-api-0.8.0.tgz", + "integrity": "sha512-nmWzrA/BQZik3VBz+npRcNIu01kdBhWL0mxKmP1ciF/gTcujPTQqt027N9fc1pK9ERM8RipFhymw7RcmCyOEYA==", + "dependencies": { + "async": "^2.6.3", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "extrareqp2": "^1.0.0", + "ws": "^7.0.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@pm2/js-api/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@pm2/js-api/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==" + }, + "node_modules/@pm2/pm2-version-check": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", + "integrity": "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==", + "dependencies": { + "debug": "^4.3.1" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.24", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", + "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==", + "dev": true + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "25.0.7", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz", + "integrity": "sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/@rollup/plugin-commonjs/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", + "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz", + "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz", + "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz", + "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz", + "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz", + "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz", + "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz", + "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz", + "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz", + "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz", + "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz", + "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz", + "integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.1.1.tgz", + "integrity": "sha512-6LeZft2Fo/4HfmLBi5CucMYmgRxgcETweQl/yQoZo/895K3S9YWYN4Sfm/IhwlIpbJp3QNvhKmwCHbsqQNYQpw==", + "dev": true, + "dependencies": { + "import-meta-resolve": "^4.0.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.0.1.tgz", + "integrity": "sha512-eYdmxdUWMW+dad1JfMsWBPY2vjXz9eE+52A2AQnXPScPJlIxIVk5mmbaEEzrZivLfO2wEcLTZ5vdC03W69x+iA==", + "dev": true, + "dependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "rollup": "^4.9.5" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.2.tgz", + "integrity": "sha512-1Pm2lsBYURQsjnLyZa+jw75eVD4gYHxGRwPyFe4DAmB3FjTVR8vRNWGeuDLGFcKMh/B1ij6FTUrc9GrerogCng==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0", + "devalue": "^4.3.2", + "esm-env": "^1.0.0", + "import-meta-resolve": "^4.0.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^2.0.4", + "tiny-glob": "^0.2.9" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.0.2.tgz", + "integrity": "sha512-MpmF/cju2HqUls50WyTHQBZUV3ovV/Uk8k66AN2gwHogNAG8wnW8xtZDhzNBsFJJuvmq1qnzA5kE7YfMJNFv2Q==", + "dev": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^2.0.0", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "svelte-hmr": "^0.15.3", + "vitefu": "^0.2.5" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.0.0.tgz", + "integrity": "sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/amp": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", + "integrity": "sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw==" + }, + "node_modules/amp-message": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/amp-message/-/amp-message-0.1.2.tgz", + "integrity": "sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg==", + "dependencies": { + "amp": "0.3.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apexcharts": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.46.0.tgz", + "integrity": "sha512-ELAY6vj8JQD7QLktKasTzwm9Wt0qxqfQSo+3QWS7G7I774iK8HCkG1toGsqJH0mkK6PtYBtnSIe66uUcwoCw1w==", + "dev": true, + "dependencies": { + "@yr/monotone-cubic-spline": "^1.0.3", + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/argparse/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-types/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/async-listener": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", + "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", + "dependencies": { + "semver": "^5.3.0", + "shimmer": "^1.1.0" + }, + "engines": { + "node": "<=0.11.8 || >0.11.10" + } + }, + "node_modules/async-listener/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.17", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", + "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.22.2", + "caniuse-lite": "^1.0.30001578", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", + "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/blessed": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz", + "integrity": "sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==", + "bin": { + "blessed": "bin/tput.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/bodec": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bodec/-/bodec-0.1.0.tgz", + "integrity": "sha512-Ylo+MAo5BDUq1KA3f3R/MFhh+g8cnHmo8bz3YPGhI1znrMaf77ol1sfvYJzsw3nTE+Y2GryfDxBaR+AqpAkEHQ==" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001589", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001589.tgz", + "integrity": "sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/charm": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/charm/-/charm-0.1.2.tgz", + "integrity": "sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cli-tableau": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cli-tableau/-/cli-tableau-2.0.1.tgz", + "integrity": "sha512-he+WTicka9cl0Fg/y+YyxcN6/bfQ/1O3QmgxRXDhABKqLzvoOSM4fMzp39uMyLBulAFuywD2N7UaoQE7WaADxQ==", + "dependencies": { + "chalk": "3.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/continuation-local-storage": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", + "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", + "dependencies": { + "async-listener": "^0.6.0", + "emitter-listener": "^1.1.1" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/croner": { + "version": "4.1.97", + "resolved": "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz", + "integrity": "sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/culvert": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/culvert/-/culvert-0.1.2.tgz", + "integrity": "sha512-yi1x3EAWKjQTreYWeSd98431AV+IEE0qoDyOoaHJ7KJ21gv6HtBXHVLX74opVSGqcR8/AbjJBHAHpcOy2bj5Gg==" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/devalue": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz", + "integrity": "sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==", + "dev": true + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.681", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.681.tgz", + "integrity": "sha512-1PpuqJUFWoXZ1E54m8bsLPVYwIVCRzvaL+n5cjigGga4z854abDnFRc+cTa2th4S79kyGqya/1xoR7h+Y5G5lg==", + "dev": true + }, + "node_modules/emitter-listener": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", + "dependencies": { + "shimmer": "^1.2.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esm-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", + "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", + "dev": true + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", + "integrity": "sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==" + }, + "node_modules/extrareqp2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/extrareqp2/-/extrareqp2-1.0.0.tgz", + "integrity": "sha512-Gum0g1QYb6wpPJCVypWP3bbIuaibcFiJcpuPM10YSXp/tzqi84x9PJageob+eN4xVRIOto4wjSGNLyMD54D2xA==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fclone": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz", + "integrity": "sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flowbite": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-2.3.0.tgz", + "integrity": "sha512-pm3JRo8OIJHGfFYWgaGpPv8E+UdWy0Z3gEAGufw+G/1dusaU/P1zoBLiQpf2/+bYAi+GBQtPVG86KYlV0W+AFQ==", + "dev": true, + "dependencies": { + "@popperjs/core": "^2.9.3", + "mini-svg-data-uri": "^1.4.3" + } + }, + "node_modules/flowbite-svelte": { + "version": "0.44.24", + "resolved": "https://registry.npmjs.org/flowbite-svelte/-/flowbite-svelte-0.44.24.tgz", + "integrity": "sha512-kXhJZHGpBVq5RFOoYnzRCEM8eFa81DVp4KjUbBsLJptKhizbSSBJuYApWIQb9pBCS8EBhX4PAX+RsgEDZfEqtA==", + "dev": true, + "dependencies": { + "@floating-ui/dom": "^1.6.3", + "apexcharts": "^3.46.0", + "flowbite": "^2.3.0", + "tailwind-merge": "^2.2.1" + }, + "engines": { + "node": ">=18.0.0", + "pnpm": ">=8.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/git-node-fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/git-node-fs/-/git-node-fs-1.0.0.tgz", + "integrity": "sha512-bLQypt14llVXBg0S0u8q8HmU7g9p3ysH+NvVlae5vILuUvs759665HvmR5+wb04KjHyjFcDRxdYb4kyNnluMUQ==" + }, + "node_modules/git-sha1": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/git-sha1/-/git-sha1-0.1.2.tgz", + "integrity": "sha512-2e/nZezdVlyCopOCYHeW0onkbZg7xP1Ad6pndPy1rCygeRykefUS6r7oA5cJRGEFvseiaz5a/qUHFVX1dd6Isg==" + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "dev": true + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", + "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-git": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/js-git/-/js-git-0.7.8.tgz", + "integrity": "sha512-+E5ZH/HeRnoc/LW0AmAyhU+mNcWBzAKE+30+IDMLSLbbK+Tdt02AdkOKq9u15rlJsDEGFqtgckc8ZM59LhhiUA==", + "dependencies": { + "bodec": "^0.1.0", + "culvert": "^0.1.2", + "git-sha1": "^0.1.2", + "pako": "^0.2.5" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "optional": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lazy": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz", + "integrity": "sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "engines": { + "node": ">=0.8.6" + } + }, + "node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/magic-string": { + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", + "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/needle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nssocket": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/nssocket/-/nssocket-0.6.0.tgz", + "integrity": "sha512-a9GSOIql5IqgWJR3F/JXG4KpJTA3Z53Cj0MeMvGpglytB1nxE4PdFNC0jINe27CS7cGivoynwc054EzCcT3M3w==", + "dependencies": { + "eventemitter2": "~0.4.14", + "lazy": "~1.0.11" + }, + "engines": { + "node": ">= 0.10.x" + } + }, + "node_modules/nssocket/node_modules/eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", + "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "pac-resolver": "^7.0.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidusage": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-3.0.2.tgz", + "integrity": "sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==", + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pm2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/pm2/-/pm2-5.3.1.tgz", + "integrity": "sha512-DLVQHpSR1EegaTaRH3KbRXxpPVaqYwAp3uHSCtCsS++LSErvk07WSxuUnntFblBRqNU/w2KQyqs12mSq5wurkg==", + "dependencies": { + "@pm2/agent": "~2.0.0", + "@pm2/io": "~5.0.0", + "@pm2/js-api": "~0.8.0", + "@pm2/pm2-version-check": "latest", + "async": "~3.2.0", + "blessed": "0.1.81", + "chalk": "3.0.0", + "chokidar": "^3.5.3", + "cli-tableau": "^2.0.0", + "commander": "2.15.1", + "croner": "~4.1.92", + "dayjs": "~1.11.5", + "debug": "^4.3.1", + "enquirer": "2.3.6", + "eventemitter2": "5.0.1", + "fclone": "1.0.11", + "mkdirp": "1.0.4", + "needle": "2.4.0", + "pidusage": "~3.0", + "pm2-axon": "~4.0.1", + "pm2-axon-rpc": "~0.7.1", + "pm2-deploy": "~1.0.2", + "pm2-multimeter": "^0.1.2", + "promptly": "^2", + "semver": "^7.2", + "source-map-support": "0.5.21", + "sprintf-js": "1.1.2", + "vizion": "~2.2.1", + "yamljs": "0.3.0" + }, + "bin": { + "pm2": "bin/pm2", + "pm2-dev": "bin/pm2-dev", + "pm2-docker": "bin/pm2-docker", + "pm2-runtime": "bin/pm2-runtime" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "pm2-sysmonit": "^1.2.8" + } + }, + "node_modules/pm2-axon": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pm2-axon/-/pm2-axon-4.0.1.tgz", + "integrity": "sha512-kES/PeSLS8orT8dR5jMlNl+Yu4Ty3nbvZRmaAtROuVm9nYYGiaoXqqKQqQYzWQzMYWUKHMQTvBlirjE5GIIxqg==", + "dependencies": { + "amp": "~0.3.1", + "amp-message": "~0.1.1", + "debug": "^4.3.1", + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=5" + } + }, + "node_modules/pm2-axon-rpc": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/pm2-axon-rpc/-/pm2-axon-rpc-0.7.1.tgz", + "integrity": "sha512-FbLvW60w+vEyvMjP/xom2UPhUN/2bVpdtLfKJeYM3gwzYhoTEEChCOICfFzxkxuoEleOlnpjie+n1nue91bDQw==", + "dependencies": { + "debug": "^4.3.1" + }, + "engines": { + "node": ">=5" + } + }, + "node_modules/pm2-deploy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pm2-deploy/-/pm2-deploy-1.0.2.tgz", + "integrity": "sha512-YJx6RXKrVrWaphEYf++EdOOx9EH18vM8RSZN/P1Y+NokTKqYAca/ejXwVLyiEpNju4HPZEk3Y2uZouwMqUlcgg==", + "dependencies": { + "run-series": "^1.1.8", + "tv4": "^1.3.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pm2-multimeter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/pm2-multimeter/-/pm2-multimeter-0.1.2.tgz", + "integrity": "sha512-S+wT6XfyKfd7SJIBqRgOctGxaBzUOmVQzTAS+cg04TsEUObJVreha7lvCfX8zzGVr871XwCSnHUU7DQQ5xEsfA==", + "dependencies": { + "charm": "~0.1.1" + } + }, + "node_modules/pm2-sysmonit": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/pm2-sysmonit/-/pm2-sysmonit-1.2.8.tgz", + "integrity": "sha512-ACOhlONEXdCTVwKieBIQLSi2tQZ8eKinhcr9JpZSUAL8Qy0ajIgRtsLxG/lwPOW3JEKqPyw/UaHmTWhUzpP4kA==", + "optional": true, + "dependencies": { + "async": "^3.2.0", + "debug": "^4.3.1", + "pidusage": "^2.0.21", + "systeminformation": "^5.7", + "tx2": "~1.0.4" + } + }, + "node_modules/pm2-sysmonit/node_modules/pidusage": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.21.tgz", + "integrity": "sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==", + "optional": true, + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "node_modules/postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-5.0.3.tgz", + "integrity": "sha512-90pBBI5apUVruIEdCxZic93Wm+i9fTrp7TXbgdUCH+/L+2WnfpITSpq5dFU/IPvbv7aNiMlQISpUkAm3fEcvgQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/promptly": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz", + "integrity": "sha512-aC9j+BZsRSSzEsXBNBwDnAxujdx19HycZoKgRgzWnS8eOHg1asuf9heuLprfbe739zY3IdUQx+Egv6Jn135WHA==", + "dependencies": { + "read": "^1.0.4" + } + }, + "node_modules/proxy-agent": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, + "node_modules/require-in-the-middle": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz", + "integrity": "sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==", + "dependencies": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz", + "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.12.0", + "@rollup/rollup-android-arm64": "4.12.0", + "@rollup/rollup-darwin-arm64": "4.12.0", + "@rollup/rollup-darwin-x64": "4.12.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.12.0", + "@rollup/rollup-linux-arm64-gnu": "4.12.0", + "@rollup/rollup-linux-arm64-musl": "4.12.0", + "@rollup/rollup-linux-riscv64-gnu": "4.12.0", + "@rollup/rollup-linux-x64-gnu": "4.12.0", + "@rollup/rollup-linux-x64-musl": "4.12.0", + "@rollup/rollup-win32-arm64-msvc": "4.12.0", + "@rollup/rollup-win32-ia32-msvc": "4.12.0", + "@rollup/rollup-win32-x64-msvc": "4.12.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/run-series": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-series/-/run-series-1.1.9.tgz", + "integrity": "sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.1.tgz", + "integrity": "sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", + "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz", + "integrity": "sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/estree": "^1.0.1", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^4.0.0", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.4", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte-hmr": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", + "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", + "dev": true, + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "dev": true, + "dependencies": { + "svg.js": "^2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "dev": true, + "dependencies": { + "svg.js": ">=2.3.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "dev": true, + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==", + "dev": true + }, + "node_modules/svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "dev": true, + "dependencies": { + "svg.js": "^2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "dev": true, + "dependencies": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js/node_modules/svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "dev": true, + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "dev": true, + "dependencies": { + "svg.js": "^2.6.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/systeminformation": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.22.0.tgz", + "integrity": "sha512-oAP80ymt8ssrAzjX8k3frbL7ys6AotqC35oikG6/SG15wBw+tG9nCk4oPaXIhEaAOAZ8XngxUv3ORq2IuR3r4Q==", + "optional": true, + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/tailwind-merge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.1.tgz", + "integrity": "sha512-o+2GTLkthfa5YUt4JxPfzMIpQzZ3adD1vLVkvKE1Twl9UAhGsEbIZhHHZVRttyW177S8PDJI3bTQNaebyofK3Q==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", + "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dev": true, + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + }, + "node_modules/tv4": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", + "integrity": "sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/tx2": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tx2/-/tx2-1.0.5.tgz", + "integrity": "sha512-sJ24w0y03Md/bxzK4FU8J8JveYYUbSs2FViLJ2D/8bytSiyPRbuE3DyL/9UKYXTZlV3yXq0L8GLlhobTnekCVg==", + "optional": true, + "dependencies": { + "json-stringify-safe": "^5.0.1" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/vite": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz", + "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vizion": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vizion/-/vizion-2.2.1.tgz", + "integrity": "sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==", + "dependencies": { + "async": "^2.6.3", + "git-node-fs": "^1.0.0", + "ini": "^1.3.5", + "js-git": "^0.7.8" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/vizion/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.0.tgz", + "integrity": "sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yamljs/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/yamljs/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/yamljs/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..143ab12 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "mz-v1-front", + "version": "0.0.1", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "start": "vite start" + }, + "devDependencies": { + "@fontsource/fira-mono": "^4.5.10", + "@neoconfetti/svelte": "^1.0.0", + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/adapter-node": "^5.0.1", + "@sveltejs/kit": "^2.5.2", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "autoprefixer": "^10.4.16", + "flowbite": "^2.3.0", + "flowbite-svelte": "^0.44.24", + "postcss": "^8.4.32", + "postcss-load-config": "^5.0.2", + "svelte": "^4.2.7", + "tailwindcss": "^3.3.6", + "vite": "^5.0.3" + }, + "type": "module", + "dependencies": { + "pm2": "^5.3.1" + } +} diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs new file mode 100644 index 0000000..e48cff5 --- /dev/null +++ b/frontend/postcss.config.cjs @@ -0,0 +1,13 @@ +const tailwindcss = require("tailwindcss"); +const autoprefixer = require("autoprefixer"); + +const config = { + plugins: [ + //Some plugins, like tailwindcss/nesting, need to run before Tailwind, + tailwindcss(), + //But others, like autoprefixer, need to run after, + autoprefixer, + ], +}; + +module.exports = config; diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 0000000..4c3707c --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/frontend/src/app.pcss b/frontend/src/app.pcss new file mode 100644 index 0000000..1a7b7cf --- /dev/null +++ b/frontend/src/app.pcss @@ -0,0 +1,4 @@ +/* Write your global styles here, in PostCSS syntax */ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/frontend/src/components/button/basic_filled.svelte b/frontend/src/components/button/basic_filled.svelte new file mode 100644 index 0000000..db3ad55 --- /dev/null +++ b/frontend/src/components/button/basic_filled.svelte @@ -0,0 +1,27 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/button/moving_filled.svelte b/frontend/src/components/button/moving_filled.svelte new file mode 100644 index 0000000..aa0c81f --- /dev/null +++ b/frontend/src/components/button/moving_filled.svelte @@ -0,0 +1,42 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/button/request_filled.svelte b/frontend/src/components/button/request_filled.svelte new file mode 100644 index 0000000..0318fdb --- /dev/null +++ b/frontend/src/components/button/request_filled.svelte @@ -0,0 +1,61 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/button/result_save.svelte b/frontend/src/components/button/result_save.svelte new file mode 100644 index 0000000..130237d --- /dev/null +++ b/frontend/src/components/button/result_save.svelte @@ -0,0 +1,30 @@ + + +save \ No newline at end of file diff --git a/frontend/src/components/header/header_login.svelte b/frontend/src/components/header/header_login.svelte new file mode 100644 index 0000000..01ef166 --- /dev/null +++ b/frontend/src/components/header/header_login.svelte @@ -0,0 +1,143 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/header/header_non.svelte b/frontend/src/components/header/header_non.svelte new file mode 100644 index 0000000..116cc2e --- /dev/null +++ b/frontend/src/components/header/header_non.svelte @@ -0,0 +1,132 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/image/PlaceholderImage.svelte b/frontend/src/components/image/PlaceholderImage.svelte new file mode 100644 index 0000000..58a409e --- /dev/null +++ b/frontend/src/components/image/PlaceholderImage.svelte @@ -0,0 +1,11 @@ + + diff --git a/frontend/src/components/image/SocialProperty1Github.svelte b/frontend/src/components/image/SocialProperty1Github.svelte new file mode 100644 index 0000000..3b55635 --- /dev/null +++ b/frontend/src/components/image/SocialProperty1Github.svelte @@ -0,0 +1,18 @@ + + diff --git a/frontend/src/components/modal/basic_modal.svelte b/frontend/src/components/modal/basic_modal.svelte new file mode 100644 index 0000000..42e630a --- /dev/null +++ b/frontend/src/components/modal/basic_modal.svelte @@ -0,0 +1,71 @@ + + + + (showModal = false)} + on:click|self={() => dialog.close()} +> + +
+

+ {p_header} +

+
+ +
+ + +
+
+ + diff --git a/frontend/src/components/rating/Star.svelte b/frontend/src/components/rating/Star.svelte new file mode 100644 index 0000000..353c2cd --- /dev/null +++ b/frontend/src/components/rating/Star.svelte @@ -0,0 +1,22 @@ + + + + + {title} + + \ No newline at end of file diff --git a/frontend/src/components/rating/StarRating.svelte b/frontend/src/components/rating/StarRating.svelte new file mode 100644 index 0000000..f40c5f7 --- /dev/null +++ b/frontend/src/components/rating/StarRating.svelte @@ -0,0 +1,143 @@ + + + + \ No newline at end of file diff --git a/frontend/src/components/survey/input_area.svelte b/frontend/src/components/survey/input_area.svelte new file mode 100644 index 0000000..6f0e195 --- /dev/null +++ b/frontend/src/components/survey/input_area.svelte @@ -0,0 +1,30 @@ + + + + + diff --git a/frontend/src/components/survey/survey.svelte b/frontend/src/components/survey/survey.svelte new file mode 100644 index 0000000..cb022a0 --- /dev/null +++ b/frontend/src/components/survey/survey.svelte @@ -0,0 +1,425 @@ + + + + + + + +
+ +
+
+

1. SNS나 인터넷, 유튜브를 하루 평균 몇 시간 사용하시나요?

+ +
+
+ +

2. 생성된 이미지들에 대해 해당 별점을 주신 이유가 무엇인가요?

+ ",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:l.htmlSerialize?[0,"",""]:[1,"X
","
"]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?""!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("