qingxu99 commited on
Commit
2af5f09
1 Parent(s): 0f0157b
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.pyc
2
+ password
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM fuqingxu/bbdown
2
+
3
+ RUN apt update && apt-get install -y python3 python3-dev python3-pip
4
+
5
+ RUN python3 -m pip install fastapi pydantic loguru --break-system-packages
6
+ RUN python3 -m pip install requests python-multipart --break-system-packages
7
+ RUN python3 -m pip install uvicorn --break-system-packages
8
+ RUN python3 -m pip install tenacity --break-system-packages
9
+
10
+ # 为了让user用户可以访问/root目录
11
+ RUN useradd -m -u 1000 user
12
+ RUN chown -R user:user /root
13
+ USER user
14
+
15
+ COPY ./docker_as_a_service /docker_as_a_service
16
+ WORKDIR /docker_as_a_service
17
+
18
+
19
+ # ENTRYPOINT [ "python3", "docker_as_a_service.py" ]
20
+ ENTRYPOINT ["/bin/bash", "-c"]
21
+ CMD ["python3 docker_as_a_service.py"]
22
+ # CMD ["python3", "docker_as_a_service.py"]
23
+ # docker build -t testbbdown .
24
+ # docker run --rm -it -p 49000:49000 testbbdown
25
+ # docker run --rm -it -p 49000:49000 --name funnn testbbdown bash
26
+ # /root/.dotnet/tools/BBDown BV1LSSHYXEtv --audio-only --use-app-api --work-dir /tmp/tmp9lrn38wb
README.md CHANGED
@@ -1,10 +1,11 @@
1
  ---
2
- title: Video Gpt
3
- emoji: 👁
4
- colorFrom: red
5
- colorTo: gray
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: VGPT
3
+ emoji: 🐳
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ app_port: 49000
9
  ---
10
 
11
+
docker_as_a_service/docker_as_a_service.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DaaS (Docker as a Service) is a service
3
+ that allows users to run docker commands on the server side.
4
+ """
5
+
6
+ from fastapi import FastAPI
7
+ from fastapi.responses import StreamingResponse
8
+ from fastapi import FastAPI, File, UploadFile, HTTPException
9
+ from pydantic import BaseModel, Field
10
+ from typing import Optional, Dict
11
+ import time
12
+ import os
13
+ import asyncio
14
+ import subprocess
15
+ import uuid
16
+ import glob
17
+ import threading
18
+ import queue
19
+ from shared_utils.docker_as_service_api import DockerServiceApiComModel
20
+
21
+ app = FastAPI()
22
+
23
+ def python_obj_to_pickle_file_bytes(obj):
24
+ import pickle
25
+ import io
26
+ with io.BytesIO() as f:
27
+ pickle.dump(obj, f)
28
+ return f.getvalue()
29
+
30
+ def yield_message(message):
31
+ dsacm = DockerServiceApiComModel(server_message=message)
32
+ return python_obj_to_pickle_file_bytes(dsacm)
33
+
34
+ def read_output(stream, output_queue):
35
+ while True:
36
+ line_stdout = stream.readline()
37
+ # print('recv')
38
+ if line_stdout:
39
+ output_queue.put(line_stdout)
40
+ else:
41
+ break
42
+
43
+
44
+ async def stream_generator(request_obj):
45
+ import tempfile
46
+ # Create a temporary directory
47
+ print('create temp dir')
48
+ with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir:
49
+
50
+ # Construct the docker command
51
+ download_folder = temp_dir
52
+
53
+ # Get list of existing files before download
54
+ existing_file_before_download = []
55
+
56
+ video_id = request_obj.client_command
57
+ cmd = [
58
+ '/root/.dotnet/tools/BBDown',
59
+ video_id,
60
+ '--use-app-api',
61
+ '--work-dir',
62
+ f'{os.path.abspath(temp_dir)}'
63
+ ]
64
+
65
+
66
+ cmd = ' '.join(cmd)
67
+ yield yield_message(cmd)
68
+ process = subprocess.Popen(cmd,
69
+ stdout=subprocess.PIPE,
70
+ stderr=subprocess.PIPE,
71
+ shell=True,
72
+ text=True)
73
+
74
+ stdout_queue = queue.Queue()
75
+ thread = threading.Thread(target=read_output, args=(process.stdout, stdout_queue))
76
+ thread.daemon = True
77
+ thread.start()
78
+ stderr_queue = queue.Queue()
79
+ thread = threading.Thread(target=read_output, args=(process.stderr, stderr_queue))
80
+ thread.daemon = True
81
+ thread.start()
82
+
83
+ while True:
84
+ print("looping")
85
+ # Check if there is any output in the queue
86
+
87
+ stdout_this_round = ""
88
+ stderr_this_round = ""
89
+ while True:
90
+ try:
91
+ output_stdout = stdout_queue.get_nowait() # Non-blocking get
92
+ if output_stdout:
93
+ stdout_this_round += output_stdout
94
+ print(output_stdout)
95
+ except queue.Empty:
96
+ yield yield_message(stdout_this_round)
97
+ break
98
+
99
+ while True:
100
+ try:
101
+ output_stderr = stderr_queue.get_nowait() # Non-blocking get
102
+ if output_stderr:
103
+ stderr_this_round += output_stderr
104
+ print(output_stderr)
105
+ except queue.Empty:
106
+ yield yield_message(stderr_this_round)
107
+ break
108
+
109
+ # Break the loop if the process has finished
110
+ if process.poll() is not None:
111
+ break
112
+
113
+ await asyncio.sleep(0.5)
114
+ print("(daas return) ")
115
+
116
+ # Get the return code
117
+ return_code = process.returncode
118
+ yield yield_message("(daas return code:) " + str(return_code))
119
+ print("(daas return code:) " + str(return_code))
120
+ # print('sleeping')
121
+ # time.sleep(9999)
122
+ # print(f"Successfully downloaded video {video_id}")
123
+ # existing_file_after_download = glob.glob(os.path.join(download_folder, '**', '*'))
124
+ existing_file_after_download = glob.glob(os.path.join(download_folder, '**'), recursive=True)
125
+
126
+ print("downloaded_files")
127
+ print(existing_file_after_download)
128
+ # existing_file_after_download = list(os.listdir(download_folder))
129
+ # get the difference
130
+ downloaded_files = [
131
+ f for f in existing_file_after_download if f not in existing_file_before_download
132
+ ]
133
+ downloaded_files_path = [
134
+ os.path.join(download_folder, f) for f in existing_file_after_download if f not in existing_file_before_download
135
+ ]
136
+ # read file
137
+ server_file_attach = {}
138
+ for fp, fn in zip(downloaded_files_path, downloaded_files):
139
+ if os.path.isdir(fp): continue
140
+ with open(fp, "rb") as f:
141
+ file_bytes = f.read()
142
+ server_file_attach[fn] = file_bytes
143
+ print("downloaded_files")
144
+ print(downloaded_files)
145
+ dsacm = DockerServiceApiComModel(
146
+ server_message="complete",
147
+ server_file_attach=server_file_attach,
148
+ )
149
+ print("sending files")
150
+ yield python_obj_to_pickle_file_bytes(dsacm)
151
+
152
+
153
+ def simple_generator(return_obj):
154
+ dsacm = DockerServiceApiComModel(
155
+ server_message=return_obj,
156
+ )
157
+ yield python_obj_to_pickle_file_bytes(dsacm)
158
+
159
+ @app.post("/stream")
160
+ async def stream_response(file: UploadFile = File(...)):
161
+ # read the file in memory, treat it as pickle file, and unpickle it
162
+ import pickle
163
+ import io
164
+ content = await file.read()
165
+ with io.BytesIO(content) as f:
166
+ request_obj = pickle.load(f)
167
+ # process the request_obj
168
+ return StreamingResponse(stream_generator(request_obj), media_type="application/octet-stream")
169
+
170
+ @app.post("/search")
171
+ async def stream_response(file: UploadFile = File(...)):
172
+ # read the file in memory, treat it as pickle file, and unpickle it
173
+ import pickle
174
+ import io
175
+ content = await file.read()
176
+ with io.BytesIO(content) as f:
177
+ request_obj = pickle.load(f)
178
+
179
+ # process the request_obj
180
+ keyword = request_obj.client_command
181
+
182
+ from experimental_mods.get_search_kw_api_stop import search_videos
183
+ # Default parameters for video search
184
+ csrf_token = '40a227fcf12c380d7d3c81af2cd8c5e8' # Using default from main()
185
+ search_type = 'default'
186
+ max_pages = 1
187
+ output_path = 'search_results'
188
+ config_path = 'experimental_mods/config.json'
189
+
190
+ # Search for videos and return the first result
191
+ videos = search_videos(
192
+ keyword=keyword,
193
+ csrf_token=csrf_token,
194
+ search_type=search_type,
195
+ max_pages=max_pages,
196
+ output_path=output_path,
197
+ config_path=config_path,
198
+ early_stop=True
199
+ )
200
+
201
+ return StreamingResponse(simple_generator(videos), media_type="application/octet-stream")
202
+
203
+ @app.get("/")
204
+ async def hi():
205
+ return "Hello, this is Docker as a Service (DaaS)! If you want to use this service, you must duplicate this space. " \
206
+ "您好,这里是Docker作为服务(DaaS)!如果您想使用此服务,您必须复制此空间。复制方法:点击https://huggingface.co/spaces/hamercity/bbdown页面右上角的三个点,然后选择“复制空间”。" \
207
+ "此外,在设置中,你还需要修改URL,例如:DAAS_SERVER_URL = \"https://你的用户名-你的空间名.hf.space/stream\""
208
+
209
+ if __name__ == "__main__":
210
+ import uvicorn
211
+ uvicorn.run(app, host="0.0.0.0", port=49000)
docker_as_a_service/docker_as_a_service_host.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DaaS (Docker as a Service) is a service
3
+ that allows users to run docker commands on the server side.
4
+ """
5
+
6
+ from fastapi import FastAPI
7
+ from fastapi.responses import StreamingResponse
8
+ from fastapi import FastAPI, File, UploadFile, HTTPException
9
+ from pydantic import BaseModel, Field
10
+ from typing import Optional, Dict
11
+ import time
12
+ import os
13
+ import asyncio
14
+ import subprocess
15
+ import uuid
16
+ import glob
17
+ import threading
18
+ import queue
19
+ from shared_utils.docker_as_service_api import DockerServiceApiComModel
20
+
21
+ app = FastAPI()
22
+
23
+ def python_obj_to_pickle_file_bytes(obj):
24
+ import pickle
25
+ import io
26
+ with io.BytesIO() as f:
27
+ pickle.dump(obj, f)
28
+ return f.getvalue()
29
+
30
+ def yield_message(message):
31
+ dsacm = DockerServiceApiComModel(server_message=message)
32
+ return python_obj_to_pickle_file_bytes(dsacm)
33
+
34
+ def read_output(stream, output_queue):
35
+ while True:
36
+ line_stdout = stream.readline()
37
+ # print('recv')
38
+ if line_stdout:
39
+ output_queue.put(line_stdout)
40
+ else:
41
+ break
42
+
43
+
44
+ async def stream_generator(request_obj):
45
+ import tempfile
46
+ # Create a temporary directory
47
+ with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir:
48
+
49
+ # Construct the docker command
50
+ download_folder = temp_dir
51
+
52
+ # Get list of existing files before download
53
+ existing_file_before_download = []
54
+
55
+ video_id = request_obj.client_command
56
+ cmd = [
57
+ '/root/.dotnet/tools/BBDown',
58
+ video_id,
59
+ '--use-app-api',
60
+ '--work-dir',
61
+ f'{os.path.abspath(temp_dir)}'
62
+ ]
63
+ cmd_chmod = []
64
+
65
+
66
+ cmd = [
67
+ 'docker', 'run', '--rm',
68
+ '-v',
69
+ f'{os.path.abspath(temp_dir)}:/downloads',
70
+ 'bbdown',
71
+ video_id,
72
+ '--use-app-api',
73
+ '--work-dir',
74
+ '/downloads'
75
+ ]
76
+ cmd_chmod = [
77
+ 'docker', 'run', '--rm',
78
+ '-v',
79
+ f'{os.path.abspath(temp_dir)}:/downloads',
80
+ '--entrypoint=""', # override the entrypoint
81
+ 'bbdown', # image name
82
+ # chmod -R 777 /downloads
83
+ 'chmod',
84
+ '-R',
85
+ '777',
86
+ '/downloads'
87
+ ]
88
+
89
+
90
+ cmd = ' '.join(cmd)
91
+ yield yield_message(cmd)
92
+ process = subprocess.Popen(cmd,
93
+ stdout=subprocess.PIPE,
94
+ stderr=subprocess.PIPE,
95
+ shell=True,
96
+ text=True)
97
+
98
+ stdout_queue = queue.Queue()
99
+ thread = threading.Thread(target=read_output, args=(process.stdout, stdout_queue))
100
+ thread.daemon = True
101
+ thread.start()
102
+ stderr_queue = queue.Queue()
103
+ thread = threading.Thread(target=read_output, args=(process.stderr, stderr_queue))
104
+ thread.daemon = True
105
+ thread.start()
106
+
107
+ while True:
108
+ print("looping")
109
+ # Check if there is any output in the queue
110
+
111
+ stdout_this_round = ""
112
+ stderr_this_round = ""
113
+ while True:
114
+ try:
115
+ output_stdout = stdout_queue.get_nowait() # Non-blocking get
116
+ if output_stdout:
117
+ stdout_this_round += output_stdout
118
+ print(output_stdout)
119
+ except queue.Empty:
120
+ yield yield_message(stdout_this_round)
121
+ break
122
+
123
+ while True:
124
+ try:
125
+ output_stderr = stderr_queue.get_nowait() # Non-blocking get
126
+ if output_stderr:
127
+ stderr_this_round += output_stderr
128
+ print(output_stderr)
129
+ except queue.Empty:
130
+ yield yield_message(stderr_this_round)
131
+ break
132
+
133
+ # Break the loop if the process has finished
134
+ if process.poll() is not None:
135
+ break
136
+
137
+ await asyncio.sleep(0.5)
138
+
139
+ # Get the return code
140
+ return_code = process.returncode
141
+ yield yield_message("(daas return code:) " + str(return_code))
142
+
143
+ # change files mod to 777
144
+ if cmd_chmod:
145
+ docker_chmod_res = subprocess.call(' '.join(cmd_chmod), shell=True)
146
+
147
+ # print(f"Successfully downloaded video {video_id}")
148
+ existing_file_after_download = glob.glob(os.path.join(download_folder, '**', '*'))
149
+ # existing_file_after_download = list(os.listdir(download_folder))
150
+ # get the difference
151
+ downloaded_files = [
152
+ f for f in existing_file_after_download if f not in existing_file_before_download
153
+ ]
154
+ downloaded_files_path = [
155
+ os.path.join(download_folder, f) for f in existing_file_after_download if f not in existing_file_before_download
156
+ ]
157
+ # read file
158
+ server_file_attach = {}
159
+ for fp, fn in zip(downloaded_files_path, downloaded_files):
160
+ if os.path.isdir(fp): continue
161
+ with open(fp, "rb") as f:
162
+ file_bytes = f.read()
163
+ server_file_attach[fn] = file_bytes
164
+
165
+ dsacm = DockerServiceApiComModel(
166
+ server_message="complete",
167
+ server_file_attach=server_file_attach,
168
+ )
169
+ yield python_obj_to_pickle_file_bytes(dsacm)
170
+
171
+
172
+ def simple_generator(return_obj):
173
+ dsacm = DockerServiceApiComModel(
174
+ server_message=return_obj,
175
+ )
176
+ yield python_obj_to_pickle_file_bytes(dsacm)
177
+
178
+ @app.post("/stream")
179
+ async def stream_response(file: UploadFile = File(...)):
180
+ # read the file in memory, treat it as pickle file, and unpickle it
181
+ import pickle
182
+ import io
183
+ content = await file.read()
184
+ with io.BytesIO(content) as f:
185
+ request_obj = pickle.load(f)
186
+ # process the request_obj
187
+ return StreamingResponse(stream_generator(request_obj), media_type="application/octet-stream")
188
+
189
+ @app.post("/search")
190
+ async def stream_response(file: UploadFile = File(...)):
191
+ # read the file in memory, treat it as pickle file, and unpickle it
192
+ import pickle
193
+ import io
194
+ content = await file.read()
195
+ with io.BytesIO(content) as f:
196
+ request_obj = pickle.load(f)
197
+
198
+ # process the request_obj
199
+ keyword = request_obj.client_command
200
+
201
+ from experimental_mods.get_search_kw_api_stop import search_videos
202
+ # Default parameters for video search
203
+ csrf_token = '40a227fcf12c380d7d3c81af2cd8c5e8' # Using default from main()
204
+ search_type = 'default'
205
+ max_pages = 1
206
+ output_path = 'search_results'
207
+ config_path = 'experimental_mods/config.json'
208
+
209
+ # Search for videos and return the first result
210
+ videos = search_videos(
211
+ keyword=keyword,
212
+ csrf_token=csrf_token,
213
+ search_type=search_type,
214
+ max_pages=max_pages,
215
+ output_path=output_path,
216
+ config_path=config_path,
217
+ early_stop=True
218
+ )
219
+
220
+ return StreamingResponse(simple_generator(videos), media_type="application/octet-stream")
221
+
222
+ @app.get("/")
223
+ async def hi():
224
+ return "Hello, this is Docker as a Service (DaaS)! If you want to use this service, you must duplicate this space. " \
225
+ "您好,这里是Docker作为服务(DaaS)!如果您想使用此服务,您必须复制此空间。复制方法:点击https://huggingface.co/spaces/hamercity/bbdown页面右上角的三个点,然后选择“复制空间”。" \
226
+ "此外,在设置中,你还需要修改URL,例如:DAAS_SERVER_URL = \"https://你的用户名-你的空间名.hf.space/stream\""
227
+
228
+ if __name__ == "__main__":
229
+ import uvicorn
230
+ uvicorn.run(app, host="0.0.0.0", port=49000)
docker_as_a_service/experimental_mods/config.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
3
+ "cookie": "buvid3=902A16D7-B19F-790B-FF85-197C38F3227462114infoc; b_nut=1731952262; b_lsid=10DF102ED6_19340664283; _uuid=84EEC61010-CE55-DAC4-68C10-CFA6108F89C8963816infoc; buvid_fp=b2f71cc1058da966a62a2caf13596b1f; buvid4=0C27B28C-406E-B88B-D232-51167891712B62902-024111817-wg%2Bfug1OO8Jl5lXoeCp0dw%3D%3D; enable_web_push=DISABLE; home_feed_column=4; browser_resolution=1313-699; bili_ticket=eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzIyMTE0NjMsImlhdCI6MTczMTk1MjIwMywicGx0IjotMX0.xKiEBdcpGFZy7Qv2wCExcBoRK-LGtvv_wvmCbuDoCN8; bili_ticket_expires=1732211403; CURRENT_FNVAL=4048; sid=5h1fpj7o; rpdid=|(k|kmJk)Y|u0J'u~JumlkkY)"
4
+ }
5
+
6
+
docker_as_a_service/experimental_mods/docker_as_a_service.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DaaS (Docker as a Service) is a service
3
+ that allows users to run docker commands on the server side.
4
+ """
5
+
6
+ from fastapi import FastAPI
7
+ from fastapi.responses import StreamingResponse
8
+ from fastapi import FastAPI, File, UploadFile, HTTPException
9
+ from pydantic import BaseModel, Field
10
+ from typing import Optional, Dict
11
+ import time
12
+ import os
13
+ import asyncio
14
+ import subprocess
15
+ import uuid
16
+ import threading
17
+ import queue
18
+
19
+ app = FastAPI()
20
+
21
+ class DockerServiceApiComModel(BaseModel):
22
+ client_command: Optional[str] = Field(default=None, title="Client command", description="The command to be executed on the client side")
23
+ client_file_attach: Optional[dict] = Field(default=None, title="Client file attach", description="The file to be attached to the client side")
24
+ server_message: Optional[str] = Field(default=None, title="Server standard error", description="The standard error from the server side")
25
+ server_std_err: Optional[str] = Field(default=None, title="Server standard error", description="The standard error from the server side")
26
+ server_std_out: Optional[str] = Field(default=None, title="Server standard output", description="The standard output from the server side")
27
+ server_file_attach: Optional[dict] = Field(default=None, title="Server file attach", description="The file to be attached to the server side")
28
+
29
+
30
+ def python_obj_to_pickle_file_bytes(obj):
31
+ import pickle
32
+ import io
33
+ with io.BytesIO() as f:
34
+ pickle.dump(obj, f)
35
+ return f.getvalue()
36
+
37
+ def yield_message(message):
38
+ dsacm = DockerServiceApiComModel(server_message=message)
39
+ return python_obj_to_pickle_file_bytes(dsacm)
40
+
41
+ def read_output(stream, output_queue):
42
+ while True:
43
+ line_stdout = stream.readline()
44
+ print('recv')
45
+ if line_stdout:
46
+ output_queue.put(line_stdout)
47
+ else:
48
+ break
49
+
50
+
51
+ async def stream_generator(request_obj):
52
+ import tempfile
53
+ # Create a temporary directory
54
+ with tempfile.TemporaryDirectory() as temp_dir:
55
+
56
+ # Construct the docker command
57
+ download_folder = temp_dir
58
+
59
+ # Get list of existing files before download
60
+ existing_file_before_download = []
61
+
62
+ video_id = request_obj.client_command
63
+ cmd = [
64
+ # 'docker', 'run', '--rm',
65
+ # '-v', f'{download_folder}:/downloads',
66
+ # 'bbdown',
67
+ # video_id,
68
+ # '--use-app-api',
69
+ # '--work-dir', '/downloads'
70
+ "while true; do date; sleep 1; done"
71
+ ]
72
+ cmd = ' '.join(cmd)
73
+ yield yield_message(cmd)
74
+ process = subprocess.Popen(cmd,
75
+ stdout=subprocess.PIPE,
76
+ stderr=subprocess.PIPE,
77
+ shell=True,
78
+ text=True)
79
+
80
+ stdout_queue = queue.Queue()
81
+ thread = threading.Thread(target=read_output, args=(process.stdout, stdout_queue))
82
+ thread.daemon = True
83
+ thread.start()
84
+ stderr_queue = queue.Queue()
85
+ thread = threading.Thread(target=read_output, args=(process.stderr, stderr_queue))
86
+ thread.daemon = True
87
+ thread.start()
88
+
89
+ while True:
90
+ print("looping")
91
+ # Check if there is any output in the queue
92
+ try:
93
+ output_stdout = stdout_queue.get_nowait() # Non-blocking get
94
+ if output_stdout:
95
+ print(output_stdout)
96
+ yield yield_message(output_stdout)
97
+
98
+ output_stderr = stderr_queue.get_nowait() # Non-blocking get
99
+ if output_stderr:
100
+ print(output_stdout)
101
+ yield yield_message(output_stderr)
102
+ except queue.Empty:
103
+ pass # No output available
104
+
105
+ # Break the loop if the process has finished
106
+ if process.poll() is not None:
107
+ break
108
+
109
+ await asyncio.sleep(0.25)
110
+
111
+ # Get the return code
112
+ return_code = process.returncode
113
+ yield yield_message("(return code:) " + str(return_code))
114
+
115
+ # print(f"Successfully downloaded video {video_id}")
116
+ existing_file_after_download = list(os.listdir(download_folder))
117
+ # get the difference
118
+ downloaded_files = [
119
+ f for f in existing_file_after_download if f not in existing_file_before_download
120
+ ]
121
+ downloaded_files_path = [
122
+ os.path.join(download_folder, f) for f in existing_file_after_download if f not in existing_file_before_download
123
+ ]
124
+ # read file
125
+ server_file_attach = {}
126
+ for fp, fn in zip(downloaded_files_path, downloaded_files):
127
+ with open(fp, "rb") as f:
128
+ file_bytes = f.read()
129
+ server_file_attach[fn] = file_bytes
130
+
131
+ dsacm = DockerServiceApiComModel(
132
+ server_message="complete",
133
+ server_file_attach=server_file_attach,
134
+ )
135
+ yield python_obj_to_pickle_file_bytes(dsacm)
136
+
137
+
138
+ @app.post("/stream")
139
+ async def stream_response(file: UploadFile = File(...)):
140
+ # read the file in memory, treat it as pickle file, and unpickle it
141
+ import pickle
142
+ import io
143
+ content = await file.read()
144
+ with io.BytesIO(content) as f:
145
+ request_obj = pickle.load(f)
146
+ # process the request_obj
147
+ return StreamingResponse(stream_generator(request_obj), media_type="application/octet-stream")
148
+
149
+ if __name__ == "__main__":
150
+ import uvicorn
151
+ uvicorn.run(app, host="127.0.0.1", port=48000)
docker_as_a_service/experimental_mods/docker_to_api copy.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pickle
3
+ from fastapi import FastAPI, File, UploadFile, HTTPException
4
+ from fastapi.responses import StreamingResponse
5
+ from io import BytesIO
6
+ import subprocess
7
+
8
+ app = FastAPI()
9
+
10
+ @app.post("/container_task")
11
+ async def container_task(file: UploadFile = File(...)):
12
+ # Save the uploaded file to disk
13
+ input_filepath = "input.pkl"
14
+ with open(input_filepath, "wb") as f:
15
+ f.write(await file.read())
16
+
17
+ # Process the unpickle_param from the file
18
+ try:
19
+ with open(input_filepath, 'rb') as f:
20
+ unpickle_param = pickle.load(f)
21
+ except Exception as e:
22
+ raise HTTPException(status_code=400, detail=f"Failed to unpickle the input file: {str(e)}")
23
+
24
+ # Execute the Docker command
25
+ command = ["docker", "run", "--rm", "bbdown", str(unpickle_param)]
26
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
27
+
28
+ # Stream the output of the command
29
+ stdout_stream = BytesIO()
30
+ stderr_stream = BytesIO()
31
+
32
+ # Create a generator to stream output
33
+ def stream_output():
34
+ while True:
35
+ output = process.stdout.readline()
36
+ if output == b"" and process.poll() is not None:
37
+ break
38
+ if output:
39
+ stdout_stream.write(output)
40
+
41
+ stderr_output = process.stderr.read()
42
+ stderr_stream.write(stderr_output)
43
+ yield ""
44
+
45
+ # Return the StreamingResponse for the current output
46
+ async def response_stream():
47
+ for _ in stream_output():
48
+ yield stdout_stream.getvalue()
49
+ stdout_stream.seek(0) # Rewind for next read
50
+ stdout_stream.truncate() # Clear for next fill
51
+
52
+ # Run the process and wait for completion
53
+ process.wait()
54
+
55
+ # Check for errors
56
+ if process.returncode != 0:
57
+ raise HTTPException(status_code=500, detail=f"Docker command failed with error: {stderr_stream.getvalue().decode()}")
58
+
59
+ # Create a new pickle file as output
60
+ output_filepath = "output.pkl"
61
+ with open(output_filepath, 'wb') as f:
62
+ f.write(b"Your output data here.") # Replace this with actual output data
63
+
64
+ # Return the output file
65
+ return StreamingResponse(open(output_filepath, "rb"), media_type='application/octet-stream',
66
+ headers={"Content-Disposition": f"attachment; filename={os.path.basename(output_filepath)}"})
67
+
68
+ # To run the application, use: uvicorn your_file_name:app --reload
69
+ from fastapi import FastAPI
70
+ from fastapi.responses import StreamingResponse
71
+ import time
72
+ import asyncio
73
+
74
+ app = FastAPI()
75
+
76
+ async def stream_generator():
77
+ for i in range(10):
78
+ yield f"Data chunk {i}\n"
79
+ await asyncio.sleep(1) # Simulating some delay
80
+
81
+ @app.get("/stream")
82
+ async def stream_response():
83
+ return StreamingResponse(stream_generator(), media_type="text/plain")
84
+
85
+ if __name__ == "__main__":
86
+ import uvicorn
87
+ uvicorn.run(app, host="127.0.0.1", port=8000)
88
+
89
+
90
+ def client_call(*args, **kwargs):
91
+
92
+ result = execute(*args, **kwargs)
93
+
94
+ result.text
95
+ result.file_manifest
96
+ result.files
97
+
docker_as_a_service/experimental_mods/get_bilibili_resource copy.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from toolbox import update_ui, get_conf, promote_file_to_downloadzone, update_ui_lastest_msg, generate_file_link
2
+
3
+
4
+ def download_bilibili(video_id, only_audio, user_name, chatbot, history):
5
+ # run : docker run --rm -v $(pwd)/downloads:/downloads bbdown BV1LSSHYXEtv --use-app-api --work-dir /downloads
6
+ import os
7
+ import subprocess
8
+ from toolbox import get_log_folder
9
+
10
+ download_folder_rel = get_log_folder(user=user_name, plugin_name="shared")
11
+ download_folder = os.path.abspath(download_folder_rel)
12
+
13
+ # Get list of existing files before download
14
+ existing_file_before_download = list(os.listdir(download_folder))
15
+
16
+ # Construct the docker command
17
+ cmd = [
18
+ 'docker', 'run', '--rm',
19
+ '-v', f'{download_folder}:/downloads',
20
+ 'bbdown',
21
+ video_id,
22
+ '--use-app-api',
23
+ '--work-dir', '/downloads'
24
+ ]
25
+ if only_audio:
26
+ cmd.append('--audio-only')
27
+
28
+
29
+ # Execute the command
30
+ result = subprocess.run(cmd, check=True, capture_output=True, text=True)
31
+ # print(f"Successfully downloaded video {video_id}")
32
+ existing_file_after_download = list(os.listdir(download_folder))
33
+ # get the difference
34
+ downloaded_files = [os.path.join(download_folder_rel, f) for f in existing_file_after_download if f not in existing_file_before_download]
35
+
36
+ return downloaded_files
37
+
docker_as_a_service/experimental_mods/get_bilibili_resource.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from toolbox import update_ui, get_conf, promote_file_to_downloadzone, update_ui_lastest_msg, generate_file_link
2
+ from shared_utils.docker_as_service_api import stream_daas
3
+ from shared_utils.docker_as_service_api import DockerServiceApiComModel
4
+
5
+ def download_bilibili(video_id, only_audio, user_name, chatbot, history):
6
+ # run : docker run --rm -v $(pwd)/downloads:/downloads bbdown BV1LSSHYXEtv --use-app-api --work-dir /downloads
7
+ import os
8
+ import subprocess
9
+ from toolbox import get_log_folder
10
+
11
+ chatbot.append([None, "Processing..."])
12
+ yield from update_ui(chatbot, history)
13
+
14
+ client_command = f'{video_id} --audio-only' if only_audio else video_id
15
+ server_url = get_conf('DAAS_SERVER_URL')
16
+ docker_service_api_com_model = DockerServiceApiComModel(client_command=client_command)
17
+ save_file_dir = get_log_folder(user_name, plugin_name='media_downloader')
18
+ for output_manifest in stream_daas(docker_service_api_com_model, server_url, save_file_dir):
19
+ status_buf = ""
20
+ status_buf += "DaaS message: \n\n"
21
+ status_buf += output_manifest['server_message'].replace('\n', '<br/>')
22
+ status_buf += "\n\n"
23
+ status_buf += "DaaS standard error: \n\n"
24
+ status_buf += output_manifest['server_std_err'].replace('\n', '<br/>')
25
+ status_buf += "\n\n"
26
+ status_buf += "DaaS standard output: \n\n"
27
+ status_buf += output_manifest['server_std_out'].replace('\n', '<br/>')
28
+ status_buf += "\n\n"
29
+ status_buf += "DaaS file attach: \n\n"
30
+ status_buf += str(output_manifest['server_file_attach'])
31
+ yield from update_ui_lastest_msg(status_buf, chatbot, history)
32
+
33
+ return output_manifest['server_file_attach']
34
+
docker_as_a_service/experimental_mods/get_search_kw_api_stop.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import time
4
+ import json
5
+ import random
6
+ import requests
7
+ import argparse
8
+ from loguru import logger
9
+ from datetime import datetime, timezone, timedelta
10
+ from typing import List, Dict
11
+ from tenacity import retry, stop_after_attempt, wait_random
12
+
13
+ def update_and_save_data(new_data: List[Dict], filename: str):
14
+ if os.path.exists(filename):
15
+ with open(filename, 'r', encoding='utf-8') as f:
16
+ existing_data = json.load(f)
17
+ else:
18
+ existing_data = []
19
+
20
+ existing_bvids = set(video['bvid'] for video in existing_data)
21
+
22
+ for video in new_data:
23
+ if video['bvid'] not in existing_bvids:
24
+ existing_data.append(video)
25
+ existing_bvids.add(video['bvid'])
26
+
27
+ with open(filename, 'w', encoding='utf-8') as f:
28
+ json.dump(existing_data, f, ensure_ascii=False, indent=4)
29
+
30
+ print(f"数据已更新并保存到 {filename}")
31
+ return existing_data
32
+
33
+ def extract_and_combine(text):
34
+ match = re.search(r'(.*?)<em class="keyword">(.*?)</em>(.*)', text)
35
+ if match:
36
+ combined = match.group(1) + match.group(2) + match.group(3)
37
+ return combined
38
+ return text
39
+
40
+ def convert_timestamp_to_beijing_time(timestamp):
41
+ utc_time = datetime.fromtimestamp(timestamp, timezone.utc)
42
+ beijing_time = utc_time + timedelta(hours=8)
43
+ return beijing_time.strftime('%Y-%m-%d %H:%M:%S')
44
+
45
+ def load_headers(config_path):
46
+ with open(config_path, 'r', encoding='utf-8') as f:
47
+ config = json.load(f)
48
+ print(f"已从 {config_path} 加载配置,请求头为:{config}")
49
+ return config
50
+
51
+
52
+ @retry(stop=stop_after_attempt(3), wait=wait_random(min=1, max=3))
53
+ def make_api_request(url, headers):
54
+ response = requests.get(url=url, headers=headers)
55
+ response.raise_for_status()
56
+ return response.json()
57
+
58
+ def search_videos(keyword, csrf_token, search_type, max_pages=5, output_path=None, config_path='config.json', early_stop=False):
59
+ url_template = "https://api.bilibili.com/x/web-interface/search/type?search_type=video&keyword={keyword}&page={page}&order={search_type}&duration=0&tids=0"
60
+ headers = load_headers(config_path)
61
+ videos = []
62
+ existing_bvids = set()
63
+
64
+ if early_stop and output_path:
65
+ output_file = f"search_results_{keyword.replace(' ', '_')}_{search_type}.json"
66
+ file_path = os.path.join(output_path, output_file)
67
+ if os.path.exists(file_path):
68
+ with open(file_path, 'r', encoding='utf-8') as f:
69
+ existing_data = json.load(f)
70
+ existing_bvids = set(video['bvid'] for video in existing_data)
71
+
72
+ for page in range(1, max_pages + 1):
73
+ url = url_template.format(keyword=keyword, page=page, search_type=search_type)
74
+ try:
75
+ data = make_api_request(url, headers)
76
+
77
+ if data['code'] != 0:
78
+ logger.error(f"Error fetching page {page}: {data['message']}")
79
+ break
80
+
81
+ if 'result' not in data['data']:
82
+ logger.info(f"No more results found on page {page}")
83
+ break
84
+
85
+ result = data['data']['result']
86
+
87
+ if not result:
88
+ logger.info(f"No more results found on page {page}")
89
+ break
90
+
91
+ new_videos = []
92
+ for video in result:
93
+ video_data = {
94
+ 'title': extract_and_combine(video['title']),
95
+ 'author': video['author'],
96
+ 'author_id': video['mid'],
97
+ 'bvid': video['bvid'],
98
+ '播放量': video['play'],
99
+ '弹幕': video['danmaku'],
100
+ '评论': video['review'],
101
+ '点赞': video['favorites'],
102
+ '发布时间': convert_timestamp_to_beijing_time(video['pubdate']),
103
+ '视频时长': video['duration'],
104
+ 'tag': video['tag'],
105
+ 'description': video['description']
106
+ }
107
+ new_videos.append(video_data)
108
+
109
+ new_bvids = set(video['bvid'] for video in new_videos)
110
+ duplicate_count = len(new_bvids.intersection(existing_bvids))
111
+ logger.info(f"Page {page}: {duplicate_count} out of {len(new_videos)} videos already exist in the dataset.")
112
+
113
+ videos.extend(new_videos)
114
+ logger.info(f"Collected {len(videos)} videos from {page} pages")
115
+ time.sleep(random.uniform(1, 3)) # Random delay between 1 and 3 seconds
116
+
117
+ except Exception as e:
118
+ logger.error(f"Error on page {page}: {str(e)}")
119
+ break
120
+
121
+ return videos
122
+
123
+ def main():
124
+ parser = argparse.ArgumentParser(description="Search for videos on Bilibili")
125
+ parser.add_argument("--keyword", default='天文馆的猫', help="Search keyword")
126
+ parser.add_argument("--csrf_token", default='40a227fcf12c380d7d3c81af2cd8c5e8', help="CSRF token for authentication")
127
+ parser.add_argument("--search_type", default='default', choices=['pubdate', 'default', 'stow', 'dm', 'click'], help="Search order type")
128
+ parser.add_argument("--max_pages", default=1, type=int, help="Maximum number of pages to fetch")
129
+ parser.add_argument("--output_path", default='search_results', help="Output directory for search results")
130
+ parser.add_argument("--interval", default=1, type=int, help="Interval in hours between searches")
131
+ parser.add_argument("--early_stop", default=True, help="Enable early stopping if all videos on a page already exist in the dataset")
132
+ args = parser.parse_args()
133
+
134
+ videos = search_videos(args.keyword, args.csrf_token, args.search_type, args.max_pages, args.output_path, early_stop=args.early_stop)
135
+ print(videos)
136
+
137
+ if __name__ == "__main__":
138
+ main()
docker_as_a_service/experimental_mods/test_docker_to_api.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import pickle
3
+ import io
4
+ import os
5
+ from pydantic import BaseModel, Field
6
+ from typing import Optional, Dict
7
+
8
+ class DockerServiceApiComModel(BaseModel):
9
+ client_command: Optional[str] = Field(default=None, title="Client command", description="The command to be executed on the client side")
10
+ client_file_attach: Optional[dict] = Field(default=None, title="Client file attach", description="The file to be attached to the client side")
11
+ server_message: Optional[str] = Field(default=None, title="Server standard error", description="The standard error from the server side")
12
+ server_std_err: Optional[str] = Field(default=None, title="Server standard error", description="The standard error from the server side")
13
+ server_std_out: Optional[str] = Field(default=None, title="Server standard output", description="The standard output from the server side")
14
+ server_file_attach: Optional[dict] = Field(default=None, title="Server file attach", description="The file to be attached to the server side")
15
+
16
+ def process_received(received: DockerServiceApiComModel, save_file_dir="./daas_output"):
17
+ # Process the received data
18
+ if received.server_message:
19
+ print(f"Recv message: {received.server_message}")
20
+ if received.server_std_err:
21
+ print(f"Recv standard error: {received.server_std_err}")
22
+ if received.server_std_out:
23
+ print(f"Recv standard output: {received.server_std_out}")
24
+ if received.server_file_attach:
25
+ # print(f"Recv file attach: {received.server_file_attach}")
26
+ for file_name, file_content in received.server_file_attach.items():
27
+ new_fp = os.path.join(save_file_dir, file_name)
28
+ new_fp_dir = os.path.dirname(new_fp)
29
+ if not os.path.exists(new_fp_dir):
30
+ os.makedirs(new_fp_dir, exist_ok=True)
31
+ with open(new_fp, 'wb') as f:
32
+ f.write(file_content)
33
+ print(f"Saved file attach to {save_file_dir}")
34
+
35
+ def send_file_and_stream_response(docker_service_api_com_model, server_url):
36
+ # Prepare the file
37
+ # Pickle the object
38
+ pickled_data = pickle.dumps(docker_service_api_com_model)
39
+
40
+ # Create a file-like object from the pickled data
41
+ file_obj = io.BytesIO(pickled_data)
42
+
43
+ # Prepare the file for sending
44
+ files = {'file': ('docker_service_api_com_model.pkl', file_obj, 'application/octet-stream')}
45
+
46
+ # Send the POST request
47
+ response = requests.post(server_url, files=files, stream=True)
48
+
49
+ max_full_package_size = 1024 * 1024 * 1024 * 1 # 1 GB
50
+
51
+ # Check if the request was successful
52
+ if response.status_code == 200:
53
+ # Process the streaming response
54
+ for chunk in response.iter_content(max_full_package_size):
55
+ if chunk:
56
+ received = pickle.loads(chunk)
57
+ process_received(received)
58
+
59
+ else:
60
+ print(f"Error: Received status code {response.status_code}")
61
+ print(response.text)
62
+
63
+ # Usage
64
+ if __name__ == "__main__":
65
+ server_url = "http://localhost:49000/stream" # Replace with your server URL
66
+ docker_service_api_com_model = DockerServiceApiComModel(
67
+ client_command='BV1LSSHYXEtv --audio-only',
68
+ )
69
+ send_file_and_stream_response(docker_service_api_com_model, server_url)
docker_as_a_service/shared_utils/advanced_markdown_format.py ADDED
@@ -0,0 +1,478 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import markdown
2
+ import re
3
+ import os
4
+ import math
5
+
6
+ from loguru import logger
7
+ from textwrap import dedent
8
+ from functools import lru_cache
9
+ from pymdownx.superfences import fence_code_format
10
+ from latex2mathml.converter import convert as tex2mathml
11
+ from shared_utils.config_loader import get_conf as get_conf
12
+ from shared_utils.text_mask import apply_gpt_academic_string_mask
13
+
14
+ markdown_extension_configs = {
15
+ "mdx_math": {
16
+ "enable_dollar_delimiter": True,
17
+ "use_gitlab_delimiters": False,
18
+ },
19
+ }
20
+
21
+ code_highlight_configs = {
22
+ "pymdownx.superfences": {
23
+ "css_class": "codehilite",
24
+ "custom_fences": [
25
+ {"name": "mermaid", "class": "mermaid", "format": fence_code_format}
26
+ ],
27
+ },
28
+ "pymdownx.highlight": {
29
+ "css_class": "codehilite",
30
+ "guess_lang": True,
31
+ # 'auto_title': True,
32
+ # 'linenums': True
33
+ },
34
+ }
35
+
36
+ code_highlight_configs_block_mermaid = {
37
+ "pymdownx.superfences": {
38
+ "css_class": "codehilite",
39
+ # "custom_fences": [
40
+ # {"name": "mermaid", "class": "mermaid", "format": fence_code_format}
41
+ # ],
42
+ },
43
+ "pymdownx.highlight": {
44
+ "css_class": "codehilite",
45
+ "guess_lang": True,
46
+ # 'auto_title': True,
47
+ # 'linenums': True
48
+ },
49
+ }
50
+
51
+
52
+ mathpatterns = {
53
+ r"(?<!\\|\$)(\$)([^\$]+)(\$)": {"allow_multi_lines": False}, #  $...$
54
+ r"(?<!\\)(\$\$)([^\$]+)(\$\$)": {"allow_multi_lines": True}, # $$...$$
55
+ r"(?<!\\)(\\\[)(.+?)(\\\])": {"allow_multi_lines": False}, # \[...\]
56
+ r'(?<!\\)(\\\()(.+?)(\\\))': {'allow_multi_lines': False}, # \(...\)
57
+ # r'(?<!\\)(\\begin{([a-z]+?\*?)})(.+?)(\\end{\2})': {'allow_multi_lines': True}, # \begin...\end
58
+ # r'(?<!\\)(\$`)([^`]+)(`\$)': {'allow_multi_lines': False}, # $`...`$
59
+ }
60
+
61
+ def tex2mathml_catch_exception(content, *args, **kwargs):
62
+ try:
63
+ content = tex2mathml(content, *args, **kwargs)
64
+ except:
65
+ content = content
66
+ return content
67
+
68
+
69
+ def replace_math_no_render(match):
70
+ content = match.group(1)
71
+ if "mode=display" in match.group(0):
72
+ content = content.replace("\n", "</br>")
73
+ return f'<font color="#00FF00">$$</font><font color="#FF00FF">{content}</font><font color="#00FF00">$$</font>'
74
+ else:
75
+ return f'<font color="#00FF00">$</font><font color="#FF00FF">{content}</font><font color="#00FF00">$</font>'
76
+
77
+
78
+ def replace_math_render(match):
79
+ content = match.group(1)
80
+ if "mode=display" in match.group(0):
81
+ if "\\begin{aligned}" in content:
82
+ content = content.replace("\\begin{aligned}", "\\begin{array}")
83
+ content = content.replace("\\end{aligned}", "\\end{array}")
84
+ content = content.replace("&", " ")
85
+ content = tex2mathml_catch_exception(content, display="block")
86
+ return content
87
+ else:
88
+ return tex2mathml_catch_exception(content)
89
+
90
+
91
+ def markdown_bug_hunt(content):
92
+ """
93
+ 解决一个mdx_math的bug(单$包裹begin命令时多余<script>)
94
+ """
95
+ content = content.replace(
96
+ '<script type="math/tex">\n<script type="math/tex; mode=display">',
97
+ '<script type="math/tex; mode=display">',
98
+ )
99
+ content = content.replace("</script>\n</script>", "</script>")
100
+ return content
101
+
102
+
103
+ def is_equation(txt):
104
+ """
105
+ 判定是否为公式 | 测试1 写出洛伦兹定律,使用tex格式公式 测试2 给出柯西不等式,使用latex格式 测试3 写出麦克斯韦方程组
106
+ """
107
+ if "```" in txt and "```reference" not in txt:
108
+ return False
109
+ if "$" not in txt and "\\[" not in txt:
110
+ return False
111
+
112
+ matches = []
113
+ for pattern, property in mathpatterns.items():
114
+ flags = re.ASCII | re.DOTALL if property["allow_multi_lines"] else re.ASCII
115
+ matches.extend(re.findall(pattern, txt, flags))
116
+ if len(matches) == 0:
117
+ return False
118
+ contain_any_eq = False
119
+ illegal_pattern = re.compile(r"[^\x00-\x7F]|echo")
120
+ for match in matches:
121
+ if len(match) != 3:
122
+ return False
123
+ eq_canidate = match[1]
124
+ if illegal_pattern.search(eq_canidate):
125
+ return False
126
+ else:
127
+ contain_any_eq = True
128
+ return contain_any_eq
129
+
130
+
131
+ def fix_markdown_indent(txt):
132
+ # fix markdown indent
133
+ if (" - " not in txt) or (". " not in txt):
134
+ # do not need to fix, fast escape
135
+ return txt
136
+ # walk through the lines and fix non-standard indentation
137
+ lines = txt.split("\n")
138
+ pattern = re.compile(r"^\s+-")
139
+ activated = False
140
+ for i, line in enumerate(lines):
141
+ if line.startswith("- ") or line.startswith("1. "):
142
+ activated = True
143
+ if activated and pattern.match(line):
144
+ stripped_string = line.lstrip()
145
+ num_spaces = len(line) - len(stripped_string)
146
+ if (num_spaces % 4) == 3:
147
+ num_spaces_should_be = math.ceil(num_spaces / 4) * 4
148
+ lines[i] = " " * num_spaces_should_be + stripped_string
149
+ return "\n".join(lines)
150
+
151
+
152
+ FENCED_BLOCK_RE = re.compile(
153
+ dedent(
154
+ r"""
155
+ (?P<fence>^[ \t]*(?:~{3,}|`{3,}))[ ]* # opening fence
156
+ ((\{(?P<attrs>[^\}\n]*)\})| # (optional {attrs} or
157
+ (\.?(?P<lang>[\w#.+-]*)[ ]*)? # optional (.)lang
158
+ (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot)[ ]*)?) # optional hl_lines)
159
+ \n # newline (end of opening fence)
160
+ (?P<code>.*?)(?<=\n) # the code block
161
+ (?P=fence)[ ]*$ # closing fence
162
+ """
163
+ ),
164
+ re.MULTILINE | re.DOTALL | re.VERBOSE,
165
+ )
166
+
167
+
168
+ def get_line_range(re_match_obj, txt):
169
+ start_pos, end_pos = re_match_obj.regs[0]
170
+ num_newlines_before = txt[: start_pos + 1].count("\n")
171
+ line_start = num_newlines_before
172
+ line_end = num_newlines_before + txt[start_pos:end_pos].count("\n") + 1
173
+ return line_start, line_end
174
+
175
+
176
+ def fix_code_segment_indent(txt):
177
+ lines = []
178
+ change_any = False
179
+ txt_tmp = txt
180
+ while True:
181
+ re_match_obj = FENCED_BLOCK_RE.search(txt_tmp)
182
+ if not re_match_obj:
183
+ break
184
+ if len(lines) == 0:
185
+ lines = txt.split("\n")
186
+
187
+ # 清空 txt_tmp 对应的位置方便下次搜索
188
+ start_pos, end_pos = re_match_obj.regs[0]
189
+ txt_tmp = txt_tmp[:start_pos] + " " * (end_pos - start_pos) + txt_tmp[end_pos:]
190
+ line_start, line_end = get_line_range(re_match_obj, txt)
191
+
192
+ # 获取公共缩进
193
+ shared_indent_cnt = 1e5
194
+ for i in range(line_start, line_end):
195
+ stripped_string = lines[i].lstrip()
196
+ num_spaces = len(lines[i]) - len(stripped_string)
197
+ if num_spaces < shared_indent_cnt:
198
+ shared_indent_cnt = num_spaces
199
+
200
+ # 修复缩进
201
+ if (shared_indent_cnt < 1e5) and (shared_indent_cnt % 4) == 3:
202
+ num_spaces_should_be = math.ceil(shared_indent_cnt / 4) * 4
203
+ for i in range(line_start, line_end):
204
+ add_n = num_spaces_should_be - shared_indent_cnt
205
+ lines[i] = " " * add_n + lines[i]
206
+ if not change_any: # 遇到第一个
207
+ change_any = True
208
+
209
+ if change_any:
210
+ return "\n".join(lines)
211
+ else:
212
+ return txt
213
+
214
+
215
+ def fix_dollar_sticking_bug(txt):
216
+ """
217
+ 修复不标准的dollar公式符号的问题
218
+ """
219
+ txt_result = ""
220
+ single_stack_height = 0
221
+ double_stack_height = 0
222
+ while True:
223
+ while True:
224
+ index = txt.find('$')
225
+
226
+ if index == -1:
227
+ txt_result += txt
228
+ return txt_result
229
+
230
+ if single_stack_height > 0:
231
+ if txt[:(index+1)].find('\n') > 0 or txt[:(index+1)].find('<td>') > 0 or txt[:(index+1)].find('</td>') > 0:
232
+ logger.error('公式之中出现了异常 (Unexpect element in equation)')
233
+ single_stack_height = 0
234
+ txt_result += ' $'
235
+ continue
236
+
237
+ if double_stack_height > 0:
238
+ if txt[:(index+1)].find('\n\n') > 0:
239
+ logger.error('公式之中出现了异常 (Unexpect element in equation)')
240
+ double_stack_height = 0
241
+ txt_result += '$$'
242
+ continue
243
+
244
+ is_double = (txt[index+1] == '$')
245
+ if is_double:
246
+ if single_stack_height != 0:
247
+ # add a padding
248
+ txt = txt[:(index+1)] + " " + txt[(index+1):]
249
+ continue
250
+ if double_stack_height == 0:
251
+ double_stack_height = 1
252
+ else:
253
+ double_stack_height = 0
254
+ txt_result += txt[:(index+2)]
255
+ txt = txt[(index+2):]
256
+ else:
257
+ if double_stack_height != 0:
258
+ # logger.info(txt[:(index)])
259
+ logger.info('发现异常嵌套公式')
260
+ if single_stack_height == 0:
261
+ single_stack_height = 1
262
+ else:
263
+ single_stack_height = 0
264
+ # logger.info(txt[:(index)])
265
+ txt_result += txt[:(index+1)]
266
+ txt = txt[(index+1):]
267
+ break
268
+
269
+
270
+ def markdown_convertion_for_file(txt):
271
+ """
272
+ 将Markdown格式的文本转换为HTML格式。如果包含数学公式,则先将公式转换为HTML格式。
273
+ """
274
+ from themes.theme import advanced_css
275
+ pre = f"""
276
+ <!DOCTYPE html><head><meta charset="utf-8"><title>GPT-Academic输出文档</title><style>{advanced_css}</style></head>
277
+ <body>
278
+ <div class="test_temp1" style="width:10%; height: 500px; float:left;"></div>
279
+ <div class="test_temp2" style="width:80%;padding: 40px;float:left;padding-left: 20px;padding-right: 20px;box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 8px 8px;border-radius: 10px;">
280
+ <div class="markdown-body">
281
+ """
282
+ suf = """
283
+ </div>
284
+ </div>
285
+ <div class="test_temp3" style="width:10%; height: 500px; float:left;"></div>
286
+ </body>
287
+ """
288
+
289
+ if txt.startswith(pre) and txt.endswith(suf):
290
+ # print('警告,输入了已经经过转化的字符串,二次转化可能出问题')
291
+ return txt # 已经被转化过,不需要再次转化
292
+
293
+ find_equation_pattern = r'<script type="math/tex(?:.*?)>(.*?)</script>'
294
+ txt = fix_markdown_indent(txt)
295
+ convert_stage_1 = fix_dollar_sticking_bug(txt)
296
+ # convert everything to html format
297
+ convert_stage_2 = markdown.markdown(
298
+ text=convert_stage_1,
299
+ extensions=[
300
+ "sane_lists",
301
+ "tables",
302
+ "mdx_math",
303
+ "pymdownx.superfences",
304
+ "pymdownx.highlight",
305
+ ],
306
+ extension_configs={**markdown_extension_configs, **code_highlight_configs},
307
+ )
308
+
309
+
310
+ def repl_fn(match):
311
+ content = match.group(2)
312
+ return f'<script type="math/tex">{content}</script>'
313
+
314
+ pattern = "|".join([pattern for pattern, property in mathpatterns.items() if not property["allow_multi_lines"]])
315
+ pattern = re.compile(pattern, flags=re.ASCII)
316
+ convert_stage_3 = pattern.sub(repl_fn, convert_stage_2)
317
+
318
+ convert_stage_4 = markdown_bug_hunt(convert_stage_3)
319
+
320
+ # 2. convert to rendered equation
321
+ convert_stage_5, n = re.subn(
322
+ find_equation_pattern, replace_math_render, convert_stage_4, flags=re.DOTALL
323
+ )
324
+ # cat them together
325
+ return pre + convert_stage_5 + suf
326
+
327
+ @lru_cache(maxsize=128) # 使用 lru缓存 加快转换速度
328
+ def markdown_convertion(txt):
329
+ """
330
+ 将Markdown格式的文本转换为HTML格式。如果包含数学公式,则先将公式转换为HTML格式。
331
+ """
332
+ pre = '<div class="markdown-body">'
333
+ suf = "</div>"
334
+ if txt.startswith(pre) and txt.endswith(suf):
335
+ # print('警告,输入了已经经过转化的字符串,二次转化可能出问题')
336
+ return txt # 已经被转化过,不需要再次转化
337
+
338
+ find_equation_pattern = r'<script type="math/tex(?:.*?)>(.*?)</script>'
339
+
340
+ txt = fix_markdown_indent(txt)
341
+ # txt = fix_code_segment_indent(txt)
342
+ if is_equation(txt): # 有$标识的公式符号,且没有代码段```的标识
343
+ # convert everything to html format
344
+ split = markdown.markdown(text="---")
345
+ convert_stage_1 = markdown.markdown(
346
+ text=txt,
347
+ extensions=[
348
+ "sane_lists",
349
+ "tables",
350
+ "mdx_math",
351
+ "pymdownx.superfences",
352
+ "pymdownx.highlight",
353
+ ],
354
+ extension_configs={**markdown_extension_configs, **code_highlight_configs},
355
+ )
356
+ convert_stage_1 = markdown_bug_hunt(convert_stage_1)
357
+ # 1. convert to easy-to-copy tex (do not render math)
358
+ convert_stage_2_1, n = re.subn(
359
+ find_equation_pattern,
360
+ replace_math_no_render,
361
+ convert_stage_1,
362
+ flags=re.DOTALL,
363
+ )
364
+ # 2. convert to rendered equation
365
+ convert_stage_2_2, n = re.subn(
366
+ find_equation_pattern, replace_math_render, convert_stage_1, flags=re.DOTALL
367
+ )
368
+ # cat them together
369
+ return pre + convert_stage_2_1 + f"{split}" + convert_stage_2_2 + suf
370
+ else:
371
+ return (
372
+ pre
373
+ + markdown.markdown(
374
+ txt,
375
+ extensions=[
376
+ "sane_lists",
377
+ "tables",
378
+ "pymdownx.superfences",
379
+ "pymdownx.highlight",
380
+ ],
381
+ extension_configs=code_highlight_configs,
382
+ )
383
+ + suf
384
+ )
385
+
386
+
387
+ def close_up_code_segment_during_stream(gpt_reply):
388
+ """
389
+ 在gpt输出代码的中途(输出了前面的```,但还没输出完后面的```),补上后面的```
390
+
391
+ Args:
392
+ gpt_reply (str): GPT模型返回的回复字符串。
393
+
394
+ Returns:
395
+ str: 返回一个新的字符串,将输出代码片段的“后面的```”补上。
396
+
397
+ """
398
+ if "```" not in gpt_reply:
399
+ return gpt_reply
400
+ if gpt_reply.endswith("```"):
401
+ return gpt_reply
402
+
403
+ # 排除了以上两个情况,我们
404
+ segments = gpt_reply.split("```")
405
+ n_mark = len(segments) - 1
406
+ if n_mark % 2 == 1:
407
+ return gpt_reply + "\n```" # 输出代码片段中!
408
+ else:
409
+ return gpt_reply
410
+
411
+
412
+ def special_render_issues_for_mermaid(text):
413
+ # 用不太优雅的方式处理一个core_functional.py中出现的mermaid渲染特例:
414
+ # 我不希望"总结绘制脑图"prompt中的mermaid渲染出来
415
+ @lru_cache(maxsize=1)
416
+ def get_special_case():
417
+ from core_functional import get_core_functions
418
+ special_case = get_core_functions()["总结绘制脑图"]["Suffix"]
419
+ return special_case
420
+ if text.endswith(get_special_case()): text = text.replace("```mermaid", "```")
421
+ return text
422
+
423
+
424
+ def compat_non_markdown_input(text):
425
+ """
426
+ 改善非markdown输入的显示效果,例如将空格转换为&nbsp;,将换行符转换为</br>等。
427
+ """
428
+ if "```" in text:
429
+ # careful input:markdown输入
430
+ text = special_render_issues_for_mermaid(text) # 处理特殊的渲染问题
431
+ return text
432
+ elif "</div>" in text:
433
+ # careful input:html输入
434
+ return text
435
+ else:
436
+ # whatever input:非markdown输入
437
+ lines = text.split("\n")
438
+ for i, line in enumerate(lines):
439
+ lines[i] = lines[i].replace(" ", "&nbsp;") # 空格转换为&nbsp;
440
+ text = "</br>".join(lines) # 换行符转换为</br>
441
+ return text
442
+
443
+
444
+ @lru_cache(maxsize=128) # 使用lru缓存
445
+ def simple_markdown_convertion(text):
446
+ pre = '<div class="markdown-body">'
447
+ suf = "</div>"
448
+ if text.startswith(pre) and text.endswith(suf):
449
+ return text # 已经被转化过,不需要再次转化
450
+ text = compat_non_markdown_input(text) # 兼容非markdown输入
451
+ text = markdown.markdown(
452
+ text,
453
+ extensions=["pymdownx.superfences", "tables", "pymdownx.highlight"],
454
+ extension_configs=code_highlight_configs,
455
+ )
456
+ return pre + text + suf
457
+
458
+
459
+ def format_io(self, y):
460
+ """
461
+ 将输入和输出解析为HTML格式。将y中最后一项的输入部分段落化,并将输出部分的Markdown和数学公式转换为HTML格式。
462
+ """
463
+ if y is None or y == []:
464
+ return []
465
+ i_ask, gpt_reply = y[-1]
466
+ i_ask = apply_gpt_academic_string_mask(i_ask, mode="show_render")
467
+ gpt_reply = apply_gpt_academic_string_mask(gpt_reply, mode="show_render")
468
+ # 当代码输出半截的时候,试着补上后个```
469
+ if gpt_reply is not None:
470
+ gpt_reply = close_up_code_segment_during_stream(gpt_reply)
471
+ # 处理提问与输出
472
+ y[-1] = (
473
+ # 输入部分
474
+ None if i_ask is None else simple_markdown_convertion(i_ask),
475
+ # 输出部分
476
+ None if gpt_reply is None else markdown_convertion(gpt_reply),
477
+ )
478
+ return y
docker_as_a_service/shared_utils/char_visual_effect.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def is_full_width_char(ch):
2
+ """判断给定的单个字符是否是全角字符"""
3
+ if '\u4e00' <= ch <= '\u9fff':
4
+ return True # 中文字符
5
+ if '\uff01' <= ch <= '\uff5e':
6
+ return True # 全角符号
7
+ if '\u3000' <= ch <= '\u303f':
8
+ return True # CJK标点符号
9
+ return False
10
+
11
+ def scolling_visual_effect(text, scroller_max_len):
12
+ text = text.\
13
+ replace('\n', '').replace('`', '.').replace(' ', '.').replace('<br/>', '.....').replace('$', '.')
14
+ place_take_cnt = 0
15
+ pointer = len(text) - 1
16
+
17
+ if len(text) < scroller_max_len:
18
+ return text
19
+
20
+ while place_take_cnt < scroller_max_len and pointer > 0:
21
+ if is_full_width_char(text[pointer]): place_take_cnt += 2
22
+ else: place_take_cnt += 1
23
+ pointer -= 1
24
+
25
+ return text[pointer:]
docker_as_a_service/shared_utils/colorful.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import platform
2
+ from sys import stdout
3
+ from loguru import logger
4
+
5
+ if platform.system()=="Linux":
6
+ pass
7
+ else:
8
+ from colorama import init
9
+ init()
10
+
11
+ # Do you like the elegance of Chinese characters?
12
+ def print红(*kw,**kargs):
13
+ print("\033[0;31m",*kw,"\033[0m",**kargs)
14
+ def print绿(*kw,**kargs):
15
+ print("\033[0;32m",*kw,"\033[0m",**kargs)
16
+ def print黄(*kw,**kargs):
17
+ print("\033[0;33m",*kw,"\033[0m",**kargs)
18
+ def print蓝(*kw,**kargs):
19
+ print("\033[0;34m",*kw,"\033[0m",**kargs)
20
+ def print紫(*kw,**kargs):
21
+ print("\033[0;35m",*kw,"\033[0m",**kargs)
22
+ def print靛(*kw,**kargs):
23
+ print("\033[0;36m",*kw,"\033[0m",**kargs)
24
+
25
+ def print亮红(*kw,**kargs):
26
+ print("\033[1;31m",*kw,"\033[0m",**kargs)
27
+ def print亮绿(*kw,**kargs):
28
+ print("\033[1;32m",*kw,"\033[0m",**kargs)
29
+ def print亮黄(*kw,**kargs):
30
+ print("\033[1;33m",*kw,"\033[0m",**kargs)
31
+ def print亮蓝(*kw,**kargs):
32
+ print("\033[1;34m",*kw,"\033[0m",**kargs)
33
+ def print亮紫(*kw,**kargs):
34
+ print("\033[1;35m",*kw,"\033[0m",**kargs)
35
+ def print亮靛(*kw,**kargs):
36
+ print("\033[1;36m",*kw,"\033[0m",**kargs)
37
+
38
+ # Do you like the elegance of Chinese characters?
39
+ def sprint红(*kw):
40
+ return "\033[0;31m"+' '.join(kw)+"\033[0m"
41
+ def sprint绿(*kw):
42
+ return "\033[0;32m"+' '.join(kw)+"\033[0m"
43
+ def sprint黄(*kw):
44
+ return "\033[0;33m"+' '.join(kw)+"\033[0m"
45
+ def sprint蓝(*kw):
46
+ return "\033[0;34m"+' '.join(kw)+"\033[0m"
47
+ def sprint紫(*kw):
48
+ return "\033[0;35m"+' '.join(kw)+"\033[0m"
49
+ def sprint靛(*kw):
50
+ return "\033[0;36m"+' '.join(kw)+"\033[0m"
51
+ def sprint亮红(*kw):
52
+ return "\033[1;31m"+' '.join(kw)+"\033[0m"
53
+ def sprint亮绿(*kw):
54
+ return "\033[1;32m"+' '.join(kw)+"\033[0m"
55
+ def sprint亮黄(*kw):
56
+ return "\033[1;33m"+' '.join(kw)+"\033[0m"
57
+ def sprint亮蓝(*kw):
58
+ return "\033[1;34m"+' '.join(kw)+"\033[0m"
59
+ def sprint亮紫(*kw):
60
+ return "\033[1;35m"+' '.join(kw)+"\033[0m"
61
+ def sprint亮靛(*kw):
62
+ return "\033[1;36m"+' '.join(kw)+"\033[0m"
63
+
64
+ def log红(*kw,**kargs):
65
+ logger.opt(depth=1).info(sprint红(*kw))
66
+ def log绿(*kw,**kargs):
67
+ logger.opt(depth=1).info(sprint绿(*kw))
68
+ def log黄(*kw,**kargs):
69
+ logger.opt(depth=1).info(sprint黄(*kw))
70
+ def log蓝(*kw,**kargs):
71
+ logger.opt(depth=1).info(sprint蓝(*kw))
72
+ def log紫(*kw,**kargs):
73
+ logger.opt(depth=1).info(sprint紫(*kw))
74
+ def log靛(*kw,**kargs):
75
+ logger.opt(depth=1).info(sprint靛(*kw))
76
+
77
+ def log亮红(*kw,**kargs):
78
+ logger.opt(depth=1).info(sprint亮红(*kw))
79
+ def log亮绿(*kw,**kargs):
80
+ logger.opt(depth=1).info(sprint亮绿(*kw))
81
+ def log亮黄(*kw,**kargs):
82
+ logger.opt(depth=1).info(sprint亮黄(*kw))
83
+ def log亮蓝(*kw,**kargs):
84
+ logger.opt(depth=1).info(sprint亮蓝(*kw))
85
+ def log亮紫(*kw,**kargs):
86
+ logger.opt(depth=1).info(sprint亮紫(*kw))
87
+ def log亮靛(*kw,**kargs):
88
+ logger.opt(depth=1).info(sprint亮靛(*kw))
docker_as_a_service/shared_utils/config_loader.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib
2
+ import time
3
+ import os
4
+ from functools import lru_cache
5
+ from shared_utils.colorful import log亮红, log亮绿, log亮蓝
6
+
7
+ pj = os.path.join
8
+ default_user_name = 'default_user'
9
+
10
+ def read_env_variable(arg, default_value):
11
+ """
12
+ 环境变量可以是 `GPT_ACADEMIC_CONFIG`(优先),也可以直接是`CONFIG`
13
+ 例如在windows cmd中,既可以写:
14
+ set USE_PROXY=True
15
+ set API_KEY=sk-j7caBpkRoxxxxxxxxxxxxxxxxxxxxxxxxxxxx
16
+ set proxies={"http":"http://127.0.0.1:10085", "https":"http://127.0.0.1:10085",}
17
+ set AVAIL_LLM_MODELS=["gpt-3.5-turbo", "chatglm"]
18
+ set AUTHENTICATION=[("username", "password"), ("username2", "password2")]
19
+ 也可以写:
20
+ set GPT_ACADEMIC_USE_PROXY=True
21
+ set GPT_ACADEMIC_API_KEY=sk-j7caBpkRoxxxxxxxxxxxxxxxxxxxxxxxxxxxx
22
+ set GPT_ACADEMIC_proxies={"http":"http://127.0.0.1:10085", "https":"http://127.0.0.1:10085",}
23
+ set GPT_ACADEMIC_AVAIL_LLM_MODELS=["gpt-3.5-turbo", "chatglm"]
24
+ set GPT_ACADEMIC_AUTHENTICATION=[("username", "password"), ("username2", "password2")]
25
+ """
26
+ arg_with_prefix = "GPT_ACADEMIC_" + arg
27
+ if arg_with_prefix in os.environ:
28
+ env_arg = os.environ[arg_with_prefix]
29
+ elif arg in os.environ:
30
+ env_arg = os.environ[arg]
31
+ else:
32
+ raise KeyError
33
+ log亮绿(f"[ENV_VAR] 尝试加载{arg},默认值:{default_value} --> 修正值:{env_arg}")
34
+ try:
35
+ if isinstance(default_value, bool):
36
+ env_arg = env_arg.strip()
37
+ if env_arg == 'True': r = True
38
+ elif env_arg == 'False': r = False
39
+ else: log亮红('Expect `True` or `False`, but have:', env_arg); r = default_value
40
+ elif isinstance(default_value, int):
41
+ r = int(env_arg)
42
+ elif isinstance(default_value, float):
43
+ r = float(env_arg)
44
+ elif isinstance(default_value, str):
45
+ r = env_arg.strip()
46
+ elif isinstance(default_value, dict):
47
+ r = eval(env_arg)
48
+ elif isinstance(default_value, list):
49
+ r = eval(env_arg)
50
+ elif default_value is None:
51
+ assert arg == "proxies"
52
+ r = eval(env_arg)
53
+ else:
54
+ log亮红(f"[ENV_VAR] 环境变量{arg}不支持通过环境变量设置! ")
55
+ raise KeyError
56
+ except:
57
+ log亮红(f"[ENV_VAR] 环境变量{arg}加载失败! ")
58
+ raise KeyError(f"[ENV_VAR] 环境变量{arg}加载失败! ")
59
+
60
+ log亮绿(f"[ENV_VAR] 成功读取环境变量{arg}")
61
+ return r
62
+
63
+
64
+ @lru_cache(maxsize=128)
65
+ def read_single_conf_with_lru_cache(arg):
66
+ from shared_utils.key_pattern_manager import is_any_api_key
67
+ try:
68
+ # 优先级1. 获取环境变量作为配置
69
+ default_ref = getattr(importlib.import_module('config'), arg) # 读取默认值作为数据类型转换的参考
70
+ r = read_env_variable(arg, default_ref)
71
+ except:
72
+ try:
73
+ # 优先级2. 获取config_private中的配置
74
+ r = getattr(importlib.import_module('config_private'), arg)
75
+ except:
76
+ # 优先级3. 获取config中的配置
77
+ r = getattr(importlib.import_module('config'), arg)
78
+
79
+ # 在读取API_KEY时,检查一下是不是忘了改config
80
+ if arg == 'API_URL_REDIRECT':
81
+ oai_rd = r.get("https://api.openai.com/v1/chat/completions", None) # API_URL_REDIRECT填写格式是错误的,请阅读`https://github.com/binary-husky/gpt_academic/wiki/项目配置说明`
82
+ if oai_rd and not oai_rd.endswith('/completions'):
83
+ log亮红("\n\n[API_URL_REDIRECT] API_URL_REDIRECT填错了。请阅读`https://github.com/binary-husky/gpt_academic/wiki/项目配置说明`。如果您确信自己没填错,无视此消息即可。")
84
+ time.sleep(5)
85
+ if arg == 'API_KEY':
86
+ log亮蓝(f"[API_KEY] 本项目现已支持OpenAI和Azure的api-key。也支持同时填写多个api-key,如API_KEY=\"openai-key1,openai-key2,azure-key3\"")
87
+ log亮蓝(f"[API_KEY] 您既可以在config.py中修改api-key(s),也可以在问题输入区输入临时的api-key(s),然后回车键提交后即可生效。")
88
+ if is_any_api_key(r):
89
+ log亮绿(f"[API_KEY] 您的 API_KEY 是: {r[:15]}*** API_KEY 导入成功")
90
+ else:
91
+ log亮红(f"[API_KEY] 您的 API_KEY({r[:15]}***)不满足任何一种已知的密钥格式,请在config文件中修改API密钥之后再运行(详见`https://github.com/binary-husky/gpt_academic/wiki/api_key`)。")
92
+ if arg == 'proxies':
93
+ if not read_single_conf_with_lru_cache('USE_PROXY'): r = None # 检查USE_PROXY,防止proxies单独起作用
94
+ if r is None:
95
+ log亮红('[PROXY] 网络代理状态:未配置。无代理状态下很可能无法访问OpenAI家族的模型。建议:检查USE_PROXY选项是否修改。')
96
+ else:
97
+ log亮绿('[PROXY] 网络代理状态:已配置。配置信息如下:', str(r))
98
+ assert isinstance(r, dict), 'proxies格式错误,请注意proxies选项的格式,不要遗漏括号。'
99
+ return r
100
+
101
+
102
+ @lru_cache(maxsize=128)
103
+ def get_conf(*args):
104
+ """
105
+ 本项目的所有配置都集中在config.py中。 修改配置有三种方法,您只需要选择其中一种即可:
106
+ - 直接修改config.py
107
+ - 创建并修改config_private.py
108
+ - 修改环境变量(修改docker-compose.yml等价于修改容器内部的环境变量)
109
+
110
+ 注意:如果您使用docker-compose部署,请修改docker-compose(等价于修改容器内部的环境变量)
111
+ """
112
+ res = []
113
+ for arg in args:
114
+ r = read_single_conf_with_lru_cache(arg)
115
+ res.append(r)
116
+ if len(res) == 1: return res[0]
117
+ return res
118
+
119
+
120
+ def set_conf(key, value):
121
+ from toolbox import read_single_conf_with_lru_cache
122
+ read_single_conf_with_lru_cache.cache_clear()
123
+ get_conf.cache_clear()
124
+ os.environ[key] = str(value)
125
+ altered = get_conf(key)
126
+ return altered
127
+
128
+
129
+ def set_multi_conf(dic):
130
+ for k, v in dic.items(): set_conf(k, v)
131
+ return
docker_as_a_service/shared_utils/connect_void_terminal.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ """
4
+ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
5
+ 接驳void-terminal:
6
+ - set_conf: 在运行过程中动态地修改配置
7
+ - set_multi_conf: 在运行过程中动态地修改多个配置
8
+ - get_plugin_handle: 获取插件的句柄
9
+ - get_plugin_default_kwargs: 获取插件的默认参数
10
+ - get_chat_handle: 获取简单聊天的句柄
11
+ - get_chat_default_kwargs: 获取简单聊天的默认参数
12
+ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
13
+ """
14
+
15
+
16
+ def get_plugin_handle(plugin_name):
17
+ """
18
+ e.g. plugin_name = 'crazy_functions.Markdown_Translate->Markdown翻译指定语言'
19
+ """
20
+ import importlib
21
+
22
+ assert (
23
+ "->" in plugin_name
24
+ ), "Example of plugin_name: crazy_functions.Markdown_Translate->Markdown翻译指定语言"
25
+ module, fn_name = plugin_name.split("->")
26
+ f_hot_reload = getattr(importlib.import_module(module, fn_name), fn_name)
27
+ return f_hot_reload
28
+
29
+
30
+ def get_chat_handle():
31
+ """
32
+ Get chat function
33
+ """
34
+ from request_llms.bridge_all import predict_no_ui_long_connection
35
+
36
+ return predict_no_ui_long_connection
37
+
38
+
39
+ def get_plugin_default_kwargs():
40
+ """
41
+ Get Plugin Default Arguments
42
+ """
43
+ from toolbox import ChatBotWithCookies, load_chat_cookies
44
+
45
+ cookies = load_chat_cookies()
46
+ llm_kwargs = {
47
+ "api_key": cookies["api_key"],
48
+ "llm_model": cookies["llm_model"],
49
+ "top_p": 1.0,
50
+ "max_length": None,
51
+ "temperature": 1.0,
52
+ }
53
+ chatbot = ChatBotWithCookies(llm_kwargs)
54
+
55
+ # txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt, user_request
56
+ DEFAULT_FN_GROUPS_kwargs = {
57
+ "main_input": "./README.md",
58
+ "llm_kwargs": llm_kwargs,
59
+ "plugin_kwargs": {},
60
+ "chatbot_with_cookie": chatbot,
61
+ "history": [],
62
+ "system_prompt": "You are a good AI.",
63
+ "user_request": None,
64
+ }
65
+ return DEFAULT_FN_GROUPS_kwargs
66
+
67
+
68
+ def get_chat_default_kwargs():
69
+ """
70
+ Get Chat Default Arguments
71
+ """
72
+ from toolbox import load_chat_cookies
73
+
74
+ cookies = load_chat_cookies()
75
+ llm_kwargs = {
76
+ "api_key": cookies["api_key"],
77
+ "llm_model": cookies["llm_model"],
78
+ "top_p": 1.0,
79
+ "max_length": None,
80
+ "temperature": 1.0,
81
+ }
82
+ default_chat_kwargs = {
83
+ "inputs": "Hello there, are you ready?",
84
+ "llm_kwargs": llm_kwargs,
85
+ "history": [],
86
+ "sys_prompt": "You are AI assistant",
87
+ "observe_window": None,
88
+ "console_slience": False,
89
+ }
90
+
91
+ return default_chat_kwargs
docker_as_a_service/shared_utils/cookie_manager.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import base64
3
+ from typing import Callable
4
+
5
+ def load_web_cookie_cache__fn_builder(customize_btns, cookies, predefined_btns)->Callable:
6
+ def load_web_cookie_cache(persistent_cookie_, cookies_):
7
+ import gradio as gr
8
+ from themes.theme import load_dynamic_theme, to_cookie_str, from_cookie_str, assign_user_uuid
9
+
10
+ ret = {}
11
+ for k in customize_btns:
12
+ ret.update({customize_btns[k]: gr.update(visible=False, value="")})
13
+
14
+ try: persistent_cookie_ = from_cookie_str(persistent_cookie_) # persistent cookie to dict
15
+ except: return ret
16
+
17
+ customize_fn_overwrite_ = persistent_cookie_.get("custom_bnt", {})
18
+ cookies_['customize_fn_overwrite'] = customize_fn_overwrite_
19
+ ret.update({cookies: cookies_})
20
+
21
+ for k,v in persistent_cookie_["custom_bnt"].items():
22
+ if v['Title'] == "": continue
23
+ if k in customize_btns: ret.update({customize_btns[k]: gr.update(visible=True, value=v['Title'])})
24
+ else: ret.update({predefined_btns[k]: gr.update(visible=True, value=v['Title'])})
25
+ return ret
26
+ return load_web_cookie_cache
27
+
28
+ def assign_btn__fn_builder(customize_btns, predefined_btns, cookies, web_cookie_cache)->Callable:
29
+ def assign_btn(persistent_cookie_, cookies_, basic_btn_dropdown_, basic_fn_title, basic_fn_prefix, basic_fn_suffix, clean_up=False):
30
+ import gradio as gr
31
+ from themes.theme import load_dynamic_theme, to_cookie_str, from_cookie_str, assign_user_uuid
32
+ ret = {}
33
+ # 读取之前的自定义按钮
34
+ customize_fn_overwrite_ = cookies_['customize_fn_overwrite']
35
+ # 更新新的自定义按钮
36
+ customize_fn_overwrite_.update({
37
+ basic_btn_dropdown_:
38
+ {
39
+ "Title":basic_fn_title,
40
+ "Prefix":basic_fn_prefix,
41
+ "Suffix":basic_fn_suffix,
42
+ }
43
+ }
44
+ )
45
+ if clean_up:
46
+ customize_fn_overwrite_ = {}
47
+ cookies_.update(customize_fn_overwrite_) # 更新cookie
48
+ visible = (not clean_up) and (basic_fn_title != "")
49
+ if basic_btn_dropdown_ in customize_btns:
50
+ # 是自定义按钮,不是预定义按钮
51
+ ret.update({customize_btns[basic_btn_dropdown_]: gr.update(visible=visible, value=basic_fn_title)})
52
+ else:
53
+ # 是预定义按钮
54
+ ret.update({predefined_btns[basic_btn_dropdown_]: gr.update(visible=visible, value=basic_fn_title)})
55
+ ret.update({cookies: cookies_})
56
+ try: persistent_cookie_ = from_cookie_str(persistent_cookie_) # persistent cookie to dict
57
+ except: persistent_cookie_ = {}
58
+ persistent_cookie_["custom_bnt"] = customize_fn_overwrite_ # dict update new value
59
+ persistent_cookie_ = to_cookie_str(persistent_cookie_) # persistent cookie to dict
60
+ ret.update({web_cookie_cache: persistent_cookie_}) # write persistent cookie
61
+ return ret
62
+ return assign_btn
63
+
64
+ # cookies, web_cookie_cache = make_cookie_cache()
65
+ def make_cookie_cache():
66
+ # 定义 后端state(cookies)、前端(web_cookie_cache)两兄弟
67
+ import gradio as gr
68
+ from toolbox import load_chat_cookies
69
+ # 定义cookies的后端state
70
+ cookies = gr.State(load_chat_cookies())
71
+ # 定义cookies的一个孪生的前端存储区(隐藏)
72
+ web_cookie_cache = gr.Textbox(visible=False, elem_id="web_cookie_cache")
73
+ return cookies, web_cookie_cache
74
+
75
+ # history, history_cache, history_cache_update = make_history_cache()
76
+ def make_history_cache():
77
+ # 定义 后端state(history)、前端(history_cache)、后端setter(history_cache_update)三兄弟
78
+ import gradio as gr
79
+ # 定义history的后端state
80
+ history = gr.State([])
81
+ # 定义history的一个孪生的前端存储区(隐藏)
82
+ history_cache = gr.Textbox(visible=False, elem_id="history_cache")
83
+ # 定义history_cache->history的更新方法(隐藏)。在触发这个按钮时,会先执行js代码更新history_cache,然后再执行python代码更新history
84
+ def process_history_cache(history_cache):
85
+ return json.loads(history_cache)
86
+ # 另一种更简单的setter方法
87
+ history_cache_update = gr.Button("", elem_id="elem_update_history", visible=False).click(
88
+ process_history_cache, inputs=[history_cache], outputs=[history])
89
+ return history, history_cache, history_cache_update
90
+
91
+
92
+
93
+ def create_button_with_javascript_callback(btn_value, elem_id, variant, js_callback, input_list, output_list, function, input_name_list, output_name_list):
94
+ import gradio as gr
95
+ middle_ware_component = gr.Textbox(visible=False, elem_id=elem_id+'_buffer')
96
+ def get_fn_wrap():
97
+ def fn_wrap(*args):
98
+ summary_dict = {}
99
+ for name, value in zip(input_name_list, args):
100
+ summary_dict.update({name: value})
101
+
102
+ res = function(*args)
103
+
104
+ for name, value in zip(output_name_list, res):
105
+ summary_dict.update({name: value})
106
+
107
+ summary = base64.b64encode(json.dumps(summary_dict).encode('utf8')).decode("utf-8")
108
+ return (*res, summary)
109
+ return fn_wrap
110
+
111
+ btn = gr.Button(btn_value, elem_id=elem_id, variant=variant)
112
+ call_args = ""
113
+ for name in output_name_list:
114
+ call_args += f"""Data["{name}"],"""
115
+ call_args = call_args.rstrip(",")
116
+ _js_callback = """
117
+ (base64MiddleString)=>{
118
+ console.log('hello')
119
+ const stringData = atob(base64MiddleString);
120
+ let Data = JSON.parse(stringData);
121
+ call = JS_CALLBACK_GEN;
122
+ call(CALL_ARGS);
123
+ }
124
+ """.replace("JS_CALLBACK_GEN", js_callback).replace("CALL_ARGS", call_args)
125
+
126
+ btn.click(get_fn_wrap(), input_list, output_list+[middle_ware_component]).then(None, [middle_ware_component], None, _js=_js_callback)
127
+ return btn
docker_as_a_service/shared_utils/docker_as_service_api.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import pickle
3
+ import io
4
+ import os
5
+ from pydantic import BaseModel, Field
6
+ from typing import Optional, Dict, Any
7
+ from loguru import logger
8
+
9
+ class DockerServiceApiComModel(BaseModel):
10
+ client_command: Optional[str] = Field(default=None, title="Client command", description="The command to be executed on the client side")
11
+ client_file_attach: Optional[dict] = Field(default=None, title="Client file attach", description="The file to be attached to the client side")
12
+ server_message: Optional[Any] = Field(default=None, title="Server standard error", description="The standard error from the server side")
13
+ server_std_err: Optional[str] = Field(default=None, title="Server standard error", description="The standard error from the server side")
14
+ server_std_out: Optional[str] = Field(default=None, title="Server standard output", description="The standard output from the server side")
15
+ server_file_attach: Optional[dict] = Field(default=None, title="Server file attach", description="The file to be attached to the server side")
16
+
17
+ def process_received(received: DockerServiceApiComModel, save_file_dir="./daas_output", output_manifest={}):
18
+ # Process the received data
19
+ if received.server_message:
20
+ output_manifest['server_message'] += received.server_message
21
+ if received.server_std_err:
22
+ output_manifest['server_std_err'] += received.server_std_err
23
+ if received.server_std_out:
24
+ output_manifest['server_std_out'] += received.server_std_out
25
+ if received.server_file_attach:
26
+ # print(f"Recv file attach: {received.server_file_attach}")
27
+ for file_name, file_content in received.server_file_attach.items():
28
+ new_fp = os.path.join(save_file_dir, file_name)
29
+ new_fp_dir = os.path.dirname(new_fp)
30
+ if not os.path.exists(new_fp_dir):
31
+ os.makedirs(new_fp_dir, exist_ok=True)
32
+ with open(new_fp, 'wb') as f:
33
+ f.write(file_content)
34
+ output_manifest['server_file_attach'].append(new_fp)
35
+ return output_manifest
36
+
37
+ def stream_daas(docker_service_api_com_model, server_url):
38
+ # Prepare the file
39
+ # Pickle the object
40
+ pickled_data = pickle.dumps(docker_service_api_com_model)
41
+
42
+ # Create a file-like object from the pickled data
43
+ file_obj = io.BytesIO(pickled_data)
44
+
45
+ # Prepare the file for sending
46
+ files = {'file': ('docker_service_api_com_model.pkl', file_obj, 'application/octet-stream')}
47
+
48
+ # Send the POST request
49
+ response = requests.post(server_url, files=files, stream=True)
50
+
51
+ max_full_package_size = 1024 * 1024 * 1024 * 1 # 1 GB
52
+
53
+ received_output_manifest = {}
54
+ received_output_manifest['server_message'] = ""
55
+ received_output_manifest['server_std_err'] = ""
56
+ received_output_manifest['server_std_out'] = ""
57
+ received_output_manifest['server_file_attach'] = []
58
+
59
+ # Check if the request was successful
60
+ if response.status_code == 200:
61
+ # Process the streaming response
62
+ for chunk in response.iter_content(max_full_package_size):
63
+ if chunk:
64
+ received = pickle.loads(chunk)
65
+ received_output_manifest = process_received(received, received_output_manifest)
66
+ yield received_output_manifest
67
+ else:
68
+ logger.error(f"Error: Received status code {response.status_code}, response.text: {response.text}")
69
+
70
+ return received_output_manifest
docker_as_a_service/shared_utils/fastapi_server.py ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests:
3
+
4
+ - custom_path false / no user auth:
5
+ -- upload file(yes)
6
+ -- download file(yes)
7
+ -- websocket(yes)
8
+ -- block __pycache__ access(yes)
9
+ -- rel (yes)
10
+ -- abs (yes)
11
+ -- block user access(fail) http://localhost:45013/file=gpt_log/admin/chat_secrets.log
12
+ -- fix(commit f6bf05048c08f5cd84593f7fdc01e64dec1f584a)-> block successful
13
+
14
+ - custom_path yes("/cc/gptac") / no user auth:
15
+ -- upload file(yes)
16
+ -- download file(yes)
17
+ -- websocket(yes)
18
+ -- block __pycache__ access(yes)
19
+ -- block user access(yes)
20
+
21
+ - custom_path yes("/cc/gptac/") / no user auth:
22
+ -- upload file(yes)
23
+ -- download file(yes)
24
+ -- websocket(yes)
25
+ -- block user access(yes)
26
+
27
+ - custom_path yes("/cc/gptac/") / + user auth:
28
+ -- upload file(yes)
29
+ -- download file(yes)
30
+ -- websocket(yes)
31
+ -- block user access(yes)
32
+ -- block user-wise access (yes)
33
+
34
+ - custom_path no + user auth:
35
+ -- upload file(yes)
36
+ -- download file(yes)
37
+ -- websocket(yes)
38
+ -- block user access(yes)
39
+ -- block user-wise access (yes)
40
+
41
+ queue cocurrent effectiveness
42
+ -- upload file(yes)
43
+ -- download file(yes)
44
+ -- websocket(yes)
45
+ """
46
+
47
+ import os, requests, threading, time
48
+ import uvicorn
49
+
50
+ def validate_path_safety(path_or_url, user):
51
+ from toolbox import get_conf, default_user_name
52
+ from toolbox import FriendlyException
53
+ PATH_PRIVATE_UPLOAD, PATH_LOGGING = get_conf('PATH_PRIVATE_UPLOAD', 'PATH_LOGGING')
54
+ sensitive_path = None
55
+ path_or_url = os.path.relpath(path_or_url)
56
+ if path_or_url.startswith(PATH_LOGGING): # 日志文件(按用户划分)
57
+ sensitive_path = PATH_LOGGING
58
+ elif path_or_url.startswith(PATH_PRIVATE_UPLOAD): # 用户的上传目录(按用户划分)
59
+ sensitive_path = PATH_PRIVATE_UPLOAD
60
+ elif path_or_url.startswith('tests') or path_or_url.startswith('build'): # 一个常用的测试目录
61
+ return True
62
+ else:
63
+ raise FriendlyException(f"输入文件的路径 ({path_or_url}) 存在,但位置非法。请将文件上传后再执行该任务。") # return False
64
+ if sensitive_path:
65
+ allowed_users = [user, 'autogen', 'arxiv_cache', default_user_name] # three user path that can be accessed
66
+ for user_allowed in allowed_users:
67
+ if f"{os.sep}".join(path_or_url.split(os.sep)[:2]) == os.path.join(sensitive_path, user_allowed):
68
+ return True
69
+ raise FriendlyException(f"输入文件的路径 ({path_or_url}) 存在,但属于其他用户。请将文件上传后再执行该任务。") # return False
70
+ return True
71
+
72
+ def _authorize_user(path_or_url, request, gradio_app):
73
+ from toolbox import get_conf, default_user_name
74
+ PATH_PRIVATE_UPLOAD, PATH_LOGGING = get_conf('PATH_PRIVATE_UPLOAD', 'PATH_LOGGING')
75
+ sensitive_path = None
76
+ path_or_url = os.path.relpath(path_or_url)
77
+ if path_or_url.startswith(PATH_LOGGING):
78
+ sensitive_path = PATH_LOGGING
79
+ if path_or_url.startswith(PATH_PRIVATE_UPLOAD):
80
+ sensitive_path = PATH_PRIVATE_UPLOAD
81
+ if sensitive_path:
82
+ token = request.cookies.get("access-token") or request.cookies.get("access-token-unsecure")
83
+ user = gradio_app.tokens.get(token) # get user
84
+ allowed_users = [user, 'autogen', 'arxiv_cache', default_user_name] # three user path that can be accessed
85
+ for user_allowed in allowed_users:
86
+ # exact match
87
+ if f"{os.sep}".join(path_or_url.split(os.sep)[:2]) == os.path.join(sensitive_path, user_allowed):
88
+ return True
89
+ return False # "越权访问!"
90
+ return True
91
+
92
+
93
+ class Server(uvicorn.Server):
94
+ # A server that runs in a separate thread
95
+ def install_signal_handlers(self):
96
+ pass
97
+
98
+ def run_in_thread(self):
99
+ self.thread = threading.Thread(target=self.run, daemon=True)
100
+ self.thread.start()
101
+ while not self.started:
102
+ time.sleep(5e-2)
103
+
104
+ def close(self):
105
+ self.should_exit = True
106
+ self.thread.join()
107
+
108
+
109
+ def start_app(app_block, CONCURRENT_COUNT, AUTHENTICATION, PORT, SSL_KEYFILE, SSL_CERTFILE):
110
+ import uvicorn
111
+ import fastapi
112
+ import gradio as gr
113
+ from fastapi import FastAPI
114
+ from gradio.routes import App
115
+ from toolbox import get_conf
116
+ CUSTOM_PATH, PATH_LOGGING = get_conf('CUSTOM_PATH', 'PATH_LOGGING')
117
+
118
+ # --- --- configurate gradio app block --- ---
119
+ app_block:gr.Blocks
120
+ app_block.ssl_verify = False
121
+ app_block.auth_message = '请登录'
122
+ app_block.favicon_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "docs/logo.png")
123
+ app_block.auth = AUTHENTICATION if len(AUTHENTICATION) != 0 else None
124
+ app_block.blocked_paths = ["config.py", "__pycache__", "config_private.py", "docker-compose.yml", "Dockerfile", f"{PATH_LOGGING}/admin"]
125
+ app_block.dev_mode = False
126
+ app_block.config = app_block.get_config_file()
127
+ app_block.enable_queue = True
128
+ app_block.queue(concurrency_count=CONCURRENT_COUNT)
129
+ app_block.validate_queue_settings()
130
+ app_block.show_api = False
131
+ app_block.config = app_block.get_config_file()
132
+ max_threads = 40
133
+ app_block.max_threads = max(
134
+ app_block._queue.max_thread_count if app_block.enable_queue else 0, max_threads
135
+ )
136
+ app_block.is_colab = False
137
+ app_block.is_kaggle = False
138
+ app_block.is_sagemaker = False
139
+
140
+ gradio_app = App.create_app(app_block)
141
+ for route in list(gradio_app.router.routes):
142
+ if route.path == "/proxy={url_path:path}":
143
+ gradio_app.router.routes.remove(route)
144
+ # --- --- replace gradio endpoint to forbid access to sensitive files --- ---
145
+ if len(AUTHENTICATION) > 0:
146
+ dependencies = []
147
+ endpoint = None
148
+ for route in list(gradio_app.router.routes):
149
+ if route.path == "/file/{path:path}":
150
+ gradio_app.router.routes.remove(route)
151
+ if route.path == "/file={path_or_url:path}":
152
+ dependencies = route.dependencies
153
+ endpoint = route.endpoint
154
+ gradio_app.router.routes.remove(route)
155
+ @gradio_app.get("/file/{path:path}", dependencies=dependencies)
156
+ @gradio_app.head("/file={path_or_url:path}", dependencies=dependencies)
157
+ @gradio_app.get("/file={path_or_url:path}", dependencies=dependencies)
158
+ async def file(path_or_url: str, request: fastapi.Request):
159
+ if not _authorize_user(path_or_url, request, gradio_app):
160
+ return "越权访问!"
161
+ stripped = path_or_url.lstrip().lower()
162
+ if stripped.startswith("https://") or stripped.startswith("http://"):
163
+ return "账户密码授权模式下, 禁止链接!"
164
+ if '../' in stripped:
165
+ return "非法路径!"
166
+ return await endpoint(path_or_url, request)
167
+
168
+ from fastapi import Request, status
169
+ from fastapi.responses import FileResponse, RedirectResponse
170
+ @gradio_app.get("/academic_logout")
171
+ async def logout():
172
+ response = RedirectResponse(url=CUSTOM_PATH, status_code=status.HTTP_302_FOUND)
173
+ response.delete_cookie('access-token')
174
+ response.delete_cookie('access-token-unsecure')
175
+ return response
176
+ else:
177
+ dependencies = []
178
+ endpoint = None
179
+ for route in list(gradio_app.router.routes):
180
+ if route.path == "/file/{path:path}":
181
+ gradio_app.router.routes.remove(route)
182
+ if route.path == "/file={path_or_url:path}":
183
+ dependencies = route.dependencies
184
+ endpoint = route.endpoint
185
+ gradio_app.router.routes.remove(route)
186
+ @gradio_app.get("/file/{path:path}", dependencies=dependencies)
187
+ @gradio_app.head("/file={path_or_url:path}", dependencies=dependencies)
188
+ @gradio_app.get("/file={path_or_url:path}", dependencies=dependencies)
189
+ async def file(path_or_url: str, request: fastapi.Request):
190
+ stripped = path_or_url.lstrip().lower()
191
+ if stripped.startswith("https://") or stripped.startswith("http://"):
192
+ return "账户密码授权模式下, 禁止链接!"
193
+ if '../' in stripped:
194
+ return "非法路径!"
195
+ return await endpoint(path_or_url, request)
196
+
197
+ # --- --- enable TTS (text-to-speech) functionality --- ---
198
+ TTS_TYPE = get_conf("TTS_TYPE")
199
+ if TTS_TYPE != "DISABLE":
200
+ # audio generation functionality
201
+ import httpx
202
+ from fastapi import FastAPI, Request, HTTPException
203
+ from starlette.responses import Response
204
+ async def forward_request(request: Request, method: str) -> Response:
205
+ async with httpx.AsyncClient() as client:
206
+ try:
207
+ # Forward the request to the target service
208
+ if TTS_TYPE == "EDGE_TTS":
209
+ import tempfile
210
+ import edge_tts
211
+ import wave
212
+ import uuid
213
+ from pydub import AudioSegment
214
+ json = await request.json()
215
+ voice = get_conf("EDGE_TTS_VOICE")
216
+ tts = edge_tts.Communicate(text=json['text'], voice=voice)
217
+ temp_folder = tempfile.gettempdir()
218
+ temp_file_name = str(uuid.uuid4().hex)
219
+ temp_file = os.path.join(temp_folder, f'{temp_file_name}.mp3')
220
+ await tts.save(temp_file)
221
+ try:
222
+ mp3_audio = AudioSegment.from_file(temp_file, format="mp3")
223
+ mp3_audio.export(temp_file, format="wav")
224
+ with open(temp_file, 'rb') as wav_file: t = wav_file.read()
225
+ os.remove(temp_file)
226
+ return Response(content=t)
227
+ except:
228
+ raise RuntimeError("ffmpeg未安装,无法处理EdgeTTS音频��安装方法见`https://github.com/jiaaro/pydub#getting-ffmpeg-set-up`")
229
+ if TTS_TYPE == "LOCAL_SOVITS_API":
230
+ # Forward the request to the target service
231
+ TARGET_URL = get_conf("GPT_SOVITS_URL")
232
+ body = await request.body()
233
+ resp = await client.post(TARGET_URL, content=body, timeout=60)
234
+ # Return the response from the target service
235
+ return Response(content=resp.content, status_code=resp.status_code, headers=dict(resp.headers))
236
+ except httpx.RequestError as e:
237
+ raise HTTPException(status_code=400, detail=f"Request to the target service failed: {str(e)}")
238
+ @gradio_app.post("/vits")
239
+ async def forward_post_request(request: Request):
240
+ return await forward_request(request, "POST")
241
+
242
+ # --- --- app_lifespan --- ---
243
+ from contextlib import asynccontextmanager
244
+ @asynccontextmanager
245
+ async def app_lifespan(app):
246
+ async def startup_gradio_app():
247
+ if gradio_app.get_blocks().enable_queue:
248
+ gradio_app.get_blocks().startup_events()
249
+ async def shutdown_gradio_app():
250
+ pass
251
+ await startup_gradio_app() # startup logic here
252
+ yield # The application will serve requests after this point
253
+ await shutdown_gradio_app() # cleanup/shutdown logic here
254
+
255
+ # --- --- FastAPI --- ---
256
+ fastapi_app = FastAPI(lifespan=app_lifespan)
257
+ fastapi_app.mount(CUSTOM_PATH, gradio_app)
258
+
259
+ # --- --- favicon and block fastapi api reference routes --- ---
260
+ from starlette.responses import JSONResponse
261
+ if CUSTOM_PATH != '/':
262
+ from fastapi.responses import FileResponse
263
+ @fastapi_app.get("/favicon.ico")
264
+ async def favicon():
265
+ return FileResponse(app_block.favicon_path)
266
+
267
+ @fastapi_app.middleware("http")
268
+ async def middleware(request: Request, call_next):
269
+ if request.scope['path'] in ["/docs", "/redoc", "/openapi.json"]:
270
+ return JSONResponse(status_code=404, content={"message": "Not Found"})
271
+ response = await call_next(request)
272
+ return response
273
+
274
+
275
+ # --- --- uvicorn.Config --- ---
276
+ ssl_keyfile = None if SSL_KEYFILE == "" else SSL_KEYFILE
277
+ ssl_certfile = None if SSL_CERTFILE == "" else SSL_CERTFILE
278
+ server_name = "0.0.0.0"
279
+ config = uvicorn.Config(
280
+ fastapi_app,
281
+ host=server_name,
282
+ port=PORT,
283
+ reload=False,
284
+ log_level="warning",
285
+ ssl_keyfile=ssl_keyfile,
286
+ ssl_certfile=ssl_certfile,
287
+ )
288
+ server = Server(config)
289
+ url_host_name = "localhost" if server_name == "0.0.0.0" else server_name
290
+ if ssl_keyfile is not None:
291
+ if ssl_certfile is None:
292
+ raise ValueError(
293
+ "ssl_certfile must be provided if ssl_keyfile is provided."
294
+ )
295
+ path_to_local_server = f"https://{url_host_name}:{PORT}/"
296
+ else:
297
+ path_to_local_server = f"http://{url_host_name}:{PORT}/"
298
+ if CUSTOM_PATH != '/':
299
+ path_to_local_server += CUSTOM_PATH.lstrip('/').rstrip('/') + '/'
300
+ # --- --- begin --- ---
301
+ server.run_in_thread()
302
+
303
+ # --- --- after server launch --- ---
304
+ app_block.server = server
305
+ app_block.server_name = server_name
306
+ app_block.local_url = path_to_local_server
307
+ app_block.protocol = (
308
+ "https"
309
+ if app_block.local_url.startswith("https") or app_block.is_colab
310
+ else "http"
311
+ )
312
+
313
+ if app_block.enable_queue:
314
+ app_block._queue.set_url(path_to_local_server)
315
+
316
+ forbid_proxies = {
317
+ "http": "",
318
+ "https": "",
319
+ }
320
+ requests.get(f"{app_block.local_url}startup-events", verify=app_block.ssl_verify, proxies=forbid_proxies)
321
+ app_block.is_running = True
322
+ app_block.block_thread()
docker_as_a_service/shared_utils/handle_upload.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib
2
+ import time
3
+ import inspect
4
+ import re
5
+ import os
6
+ import base64
7
+ import gradio
8
+ import shutil
9
+ import glob
10
+ from shared_utils.config_loader import get_conf
11
+ from loguru import logger
12
+
13
+ def html_local_file(file):
14
+ base_path = os.path.dirname(__file__) # 项目目录
15
+ if os.path.exists(str(file)):
16
+ file = f'file={file.replace(base_path, ".")}'
17
+ return file
18
+
19
+
20
+ def html_local_img(__file, layout="left", max_width=None, max_height=None, md=True):
21
+ style = ""
22
+ if max_width is not None:
23
+ style += f"max-width: {max_width};"
24
+ if max_height is not None:
25
+ style += f"max-height: {max_height};"
26
+ __file = html_local_file(__file)
27
+ a = f'<div align="{layout}"><img src="{__file}" style="{style}"></div>'
28
+ if md:
29
+ a = f"![{__file}]({__file})"
30
+ return a
31
+
32
+
33
+ def file_manifest_filter_type(file_list, filter_: list = None):
34
+ new_list = []
35
+ if not filter_:
36
+ filter_ = ["png", "jpg", "jpeg"]
37
+ for file in file_list:
38
+ if str(os.path.basename(file)).split(".")[-1] in filter_:
39
+ new_list.append(html_local_img(file, md=False))
40
+ else:
41
+ new_list.append(file)
42
+ return new_list
43
+
44
+
45
+ def zip_extract_member_new(self, member, targetpath, pwd):
46
+ # 修复中文乱码的问题
47
+ """Extract the ZipInfo object 'member' to a physical
48
+ file on the path targetpath.
49
+ """
50
+ import zipfile
51
+ if not isinstance(member, zipfile.ZipInfo):
52
+ member = self.getinfo(member)
53
+
54
+ # build the destination pathname, replacing
55
+ # forward slashes to platform specific separators.
56
+ arcname = member.filename.replace('/', os.path.sep)
57
+ arcname = arcname.encode('cp437', errors='replace').decode('gbk', errors='replace')
58
+
59
+ if os.path.altsep:
60
+ arcname = arcname.replace(os.path.altsep, os.path.sep)
61
+ # interpret absolute pathname as relative, remove drive letter or
62
+ # UNC path, redundant separators, "." and ".." components.
63
+ arcname = os.path.splitdrive(arcname)[1]
64
+ invalid_path_parts = ('', os.path.curdir, os.path.pardir)
65
+ arcname = os.path.sep.join(x for x in arcname.split(os.path.sep)
66
+ if x not in invalid_path_parts)
67
+ if os.path.sep == '\\':
68
+ # filter illegal characters on Windows
69
+ arcname = self._sanitize_windows_name(arcname, os.path.sep)
70
+
71
+ targetpath = os.path.join(targetpath, arcname)
72
+ targetpath = os.path.normpath(targetpath)
73
+
74
+ # Create all upper directories if necessary.
75
+ upperdirs = os.path.dirname(targetpath)
76
+ if upperdirs and not os.path.exists(upperdirs):
77
+ os.makedirs(upperdirs)
78
+
79
+ if member.is_dir():
80
+ if not os.path.isdir(targetpath):
81
+ os.mkdir(targetpath)
82
+ return targetpath
83
+
84
+ with self.open(member, pwd=pwd) as source, \
85
+ open(targetpath, "wb") as target:
86
+ shutil.copyfileobj(source, target)
87
+
88
+ return targetpath
89
+
90
+
91
+ def extract_archive(file_path, dest_dir):
92
+ import zipfile
93
+ import tarfile
94
+ import os
95
+
96
+ # Get the file extension of the input file
97
+ file_extension = os.path.splitext(file_path)[1]
98
+
99
+ # Extract the archive based on its extension
100
+ if file_extension == ".zip":
101
+ with zipfile.ZipFile(file_path, "r") as zipobj:
102
+ zipobj._extract_member = lambda a,b,c: zip_extract_member_new(zipobj, a,b,c) # 修复中文乱码的问题
103
+ zipobj.extractall(path=dest_dir)
104
+ logger.info("Successfully extracted zip archive to {}".format(dest_dir))
105
+
106
+ elif file_extension in [".tar", ".gz", ".bz2"]:
107
+ try:
108
+ with tarfile.open(file_path, "r:*") as tarobj:
109
+ # 清理提取路径,移除任何不安全的元素
110
+ for member in tarobj.getmembers():
111
+ member_path = os.path.normpath(member.name)
112
+ full_path = os.path.join(dest_dir, member_path)
113
+ full_path = os.path.abspath(full_path)
114
+ if not full_path.startswith(os.path.abspath(dest_dir) + os.sep):
115
+ raise Exception(f"Attempted Path Traversal in {member.name}")
116
+
117
+ tarobj.extractall(path=dest_dir)
118
+ logger.info("Successfully extracted tar archive to {}".format(dest_dir))
119
+ except tarfile.ReadError as e:
120
+ if file_extension == ".gz":
121
+ # 一些特别奇葩的项目,是一个gz文件,里面不是tar,只有一个tex文件
122
+ import gzip
123
+ with gzip.open(file_path, 'rb') as f_in:
124
+ with open(os.path.join(dest_dir, 'main.tex'), 'wb') as f_out:
125
+ f_out.write(f_in.read())
126
+ else:
127
+ raise e
128
+
129
+ # 第三方库,需要预先pip install rarfile
130
+ # 此外,Windows上还需要安装winrar软件,配置其Path环境变量,如"C:\Program Files\WinRAR"才可以
131
+ elif file_extension == ".rar":
132
+ try:
133
+ import rarfile
134
+
135
+ with rarfile.RarFile(file_path) as rf:
136
+ rf.extractall(path=dest_dir)
137
+ logger.info("Successfully extracted rar archive to {}".format(dest_dir))
138
+ except:
139
+ logger.info("Rar format requires additional dependencies to install")
140
+ return "\n\n解压失败! 需要安装pip install rarfile来解压rar文件。建议:使用zip压缩格式。"
141
+
142
+ # 第三方库,需要预先pip install py7zr
143
+ elif file_extension == ".7z":
144
+ try:
145
+ import py7zr
146
+
147
+ with py7zr.SevenZipFile(file_path, mode="r") as f:
148
+ f.extractall(path=dest_dir)
149
+ logger.info("Successfully extracted 7z archive to {}".format(dest_dir))
150
+ except:
151
+ logger.info("7z format requires additional dependencies to install")
152
+ return "\n\n解压失败! 需要安装pip install py7zr来解压7z文件"
153
+ else:
154
+ return ""
155
+ return ""
156
+
docker_as_a_service/shared_utils/key_pattern_manager.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import os
3
+ from functools import wraps, lru_cache
4
+ from shared_utils.advanced_markdown_format import format_io
5
+ from shared_utils.config_loader import get_conf as get_conf
6
+
7
+
8
+ pj = os.path.join
9
+ default_user_name = 'default_user'
10
+
11
+ # match openai keys
12
+ openai_regex = re.compile(
13
+ r"sk-[a-zA-Z0-9_-]{48}$|" +
14
+ r"sk-[a-zA-Z0-9_-]{92}$|" +
15
+ r"sk-proj-[a-zA-Z0-9_-]{48}$|"+
16
+ r"sk-proj-[a-zA-Z0-9_-]{124}$|"+
17
+ r"sk-proj-[a-zA-Z0-9_-]{156}$|"+ #新版apikey位数不匹配故修改此正则表达式
18
+ r"sess-[a-zA-Z0-9]{40}$"
19
+ )
20
+ def is_openai_api_key(key):
21
+ CUSTOM_API_KEY_PATTERN = get_conf('CUSTOM_API_KEY_PATTERN')
22
+ if len(CUSTOM_API_KEY_PATTERN) != 0:
23
+ API_MATCH_ORIGINAL = re.match(CUSTOM_API_KEY_PATTERN, key)
24
+ else:
25
+ API_MATCH_ORIGINAL = openai_regex.match(key)
26
+ return bool(API_MATCH_ORIGINAL)
27
+
28
+
29
+ def is_azure_api_key(key):
30
+ API_MATCH_AZURE = re.match(r"[a-zA-Z0-9]{32}$", key)
31
+ return bool(API_MATCH_AZURE)
32
+
33
+
34
+ def is_api2d_key(key):
35
+ API_MATCH_API2D = re.match(r"fk[a-zA-Z0-9]{6}-[a-zA-Z0-9]{32}$", key)
36
+ return bool(API_MATCH_API2D)
37
+
38
+ def is_openroute_api_key(key):
39
+ API_MATCH_OPENROUTE = re.match(r"sk-or-v1-[a-zA-Z0-9]{64}$", key)
40
+ return bool(API_MATCH_OPENROUTE)
41
+
42
+ def is_cohere_api_key(key):
43
+ API_MATCH_AZURE = re.match(r"[a-zA-Z0-9]{40}$", key)
44
+ return bool(API_MATCH_AZURE)
45
+
46
+
47
+ def is_any_api_key(key):
48
+ if ',' in key:
49
+ keys = key.split(',')
50
+ for k in keys:
51
+ if is_any_api_key(k): return True
52
+ return False
53
+ else:
54
+ return is_openai_api_key(key) or is_api2d_key(key) or is_azure_api_key(key) or is_cohere_api_key(key)
55
+
56
+
57
+ def what_keys(keys):
58
+ avail_key_list = {'OpenAI Key': 0, "Azure Key": 0, "API2D Key": 0}
59
+ key_list = keys.split(',')
60
+
61
+ for k in key_list:
62
+ if is_openai_api_key(k):
63
+ avail_key_list['OpenAI Key'] += 1
64
+
65
+ for k in key_list:
66
+ if is_api2d_key(k):
67
+ avail_key_list['API2D Key'] += 1
68
+
69
+ for k in key_list:
70
+ if is_azure_api_key(k):
71
+ avail_key_list['Azure Key'] += 1
72
+
73
+ return f"检测到: OpenAI Key {avail_key_list['OpenAI Key']} 个, Azure Key {avail_key_list['Azure Key']} 个, API2D Key {avail_key_list['API2D Key']} 个"
74
+
75
+
76
+ def select_api_key(keys, llm_model):
77
+ import random
78
+ avail_key_list = []
79
+ key_list = keys.split(',')
80
+
81
+ if llm_model.startswith('gpt-') or llm_model.startswith('one-api-') or llm_model.startswith('o1-'):
82
+ for k in key_list:
83
+ if is_openai_api_key(k): avail_key_list.append(k)
84
+
85
+ if llm_model.startswith('api2d-'):
86
+ for k in key_list:
87
+ if is_api2d_key(k): avail_key_list.append(k)
88
+
89
+ if llm_model.startswith('azure-'):
90
+ for k in key_list:
91
+ if is_azure_api_key(k): avail_key_list.append(k)
92
+
93
+ if llm_model.startswith('cohere-'):
94
+ for k in key_list:
95
+ if is_cohere_api_key(k): avail_key_list.append(k)
96
+
97
+ if llm_model.startswith('openrouter-'):
98
+ for k in key_list:
99
+ if is_openroute_api_key(k): avail_key_list.append(k)
100
+
101
+ if len(avail_key_list) == 0:
102
+ raise RuntimeError(f"您提供的api-key不满足要求,不包含任何可用于{llm_model}的api-key。您可能选择了错误的模型或请求源(左上角更换模型菜单中可切换openai,azure,claude,cohere等请求源)。")
103
+
104
+ api_key = random.choice(avail_key_list) # 随机负载均衡
105
+ return api_key
106
+
107
+
108
+ def select_api_key_for_embed_models(keys, llm_model):
109
+ import random
110
+ avail_key_list = []
111
+ key_list = keys.split(',')
112
+
113
+ if llm_model.startswith('text-embedding-'):
114
+ for k in key_list:
115
+ if is_openai_api_key(k): avail_key_list.append(k)
116
+
117
+ if len(avail_key_list) == 0:
118
+ raise RuntimeError(f"您提供的api-key不满足要求,不包含任何可用于{llm_model}的api-key。您可能选择了错误的模型或请求源。")
119
+
120
+ api_key = random.choice(avail_key_list) # 随机负载均衡
121
+ return api_key
docker_as_a_service/shared_utils/logging.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from loguru import logger
2
+ import logging
3
+ import sys
4
+ import os
5
+
6
+ def chat_log_filter(record):
7
+ return "chat_msg" in record["extra"]
8
+
9
+ def not_chat_log_filter(record):
10
+ return "chat_msg" not in record["extra"]
11
+
12
+ def formatter_with_clip(record):
13
+ # Note this function returns the string to be formatted, not the actual message to be logged
14
+ # record["extra"]["serialized"] = "555555"
15
+ max_len = 12
16
+ record['function_x'] = record['function'].center(max_len)
17
+ if len(record['function_x']) > max_len:
18
+ record['function_x'] = ".." + record['function_x'][-(max_len-2):]
19
+ record['line_x'] = str(record['line']).ljust(3)
20
+ return '<green>{time:HH:mm}</green> | <cyan>{function_x}</cyan>:<cyan>{line_x}</cyan> | <level>{message}</level>\n'
21
+
22
+ def setup_logging(PATH_LOGGING):
23
+
24
+ admin_log_path = os.path.join(PATH_LOGGING, "admin")
25
+ os.makedirs(admin_log_path, exist_ok=True)
26
+ sensitive_log_path = os.path.join(admin_log_path, "chat_secrets.log")
27
+ regular_log_path = os.path.join(admin_log_path, "console_log.log")
28
+ logger.remove()
29
+ logger.configure(
30
+ levels=[dict(name="WARNING", color="<g>")],
31
+ )
32
+
33
+ logger.add(
34
+ sys.stderr,
35
+ format=formatter_with_clip,
36
+ # format='<green>{time:HH:mm}</green> | <cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>',
37
+ filter=(lambda record: not chat_log_filter(record)),
38
+ colorize=True,
39
+ enqueue=True
40
+ )
41
+
42
+ logger.add(
43
+ sensitive_log_path,
44
+ format='<green>{time:MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>',
45
+ rotation="10 MB",
46
+ filter=chat_log_filter,
47
+ enqueue=True,
48
+ )
49
+
50
+ logger.add(
51
+ regular_log_path,
52
+ format='<green>{time:MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>',
53
+ rotation="10 MB",
54
+ filter=not_chat_log_filter,
55
+ enqueue=True,
56
+ )
57
+
58
+ logging.getLogger("httpx").setLevel(logging.WARNING)
59
+
60
+ logger.warning(f"所有对话记录将自动保存在本地目录{sensitive_log_path}, 请注意自我隐私保护哦!")
61
+
62
+
63
+ # logger.bind(chat_msg=True).info("This message is logged to the file!")
64
+ # logger.debug(f"debug message")
65
+ # logger.info(f"info message")
66
+ # logger.success(f"success message")
67
+ # logger.error(f"error message")
68
+ # logger.add("special.log", filter=lambda record: "special" in record["extra"])
69
+ # logger.debug("This message is not logged to the file")
docker_as_a_service/shared_utils/map_names.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ mapping_dic = {
3
+ # "qianfan": "qianfan(文心一言大模型)",
4
+ # "zhipuai": "zhipuai(智谱GLM4超级模型🔥)",
5
+ # "gpt-4-1106-preview": "gpt-4-1106-preview(新调优版本GPT-4🔥)",
6
+ # "gpt-4-vision-preview": "gpt-4-vision-preview(识图模型GPT-4V)",
7
+ }
8
+
9
+ rev_mapping_dic = {}
10
+ for k, v in mapping_dic.items():
11
+ rev_mapping_dic[v] = k
12
+
13
+ def map_model_to_friendly_names(m):
14
+ if m in mapping_dic:
15
+ return mapping_dic[m]
16
+ return m
17
+
18
+ def map_friendly_names_to_model(m):
19
+ if m in rev_mapping_dic:
20
+ return rev_mapping_dic[m]
21
+ return m
22
+
23
+ def read_one_api_model_name(model: str):
24
+ """return real model name and max_token.
25
+ """
26
+ max_token_pattern = r"\(max_token=(\d+)\)"
27
+ match = re.search(max_token_pattern, model)
28
+ if match:
29
+ max_token_tmp = match.group(1) # 获取 max_token 的值
30
+ max_token_tmp = int(max_token_tmp)
31
+ model = re.sub(max_token_pattern, "", model) # 从原字符串中删除 "(max_token=...)"
32
+ else:
33
+ max_token_tmp = 4096
34
+ return model, max_token_tmp
docker_as_a_service/shared_utils/text_mask.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from functools import lru_cache
3
+
4
+ # 这段代码是使用Python编程语言中的re模块,即正则表达式库,来定义了一个正则表达式模式。
5
+ # 这个模式被编译成一个正则表达式对象,存储在名为const_extract_exp的变量中,以便于后续快速的匹配和查找操作。
6
+ # 这里解释一下正则表达式中的几个特殊字符:
7
+ # - . 表示任意单一字符。
8
+ # - * 表示前一个字符可以出现0次或多次。
9
+ # - ? 在这里用作非贪婪匹配,也就是说它会匹配尽可能少的字符。在(.*?)中,它确保我们匹配的任意文本是尽可能短的,也就是说,它会在</show_llm>和</show_render>标签之前停止匹配。
10
+ # - () 括号在正则表达式中表示捕获组。
11
+ # - 在这个例子中,(.*?)表示捕获任意长度的文本,直到遇到括号外部最近的限定符,即</show_llm>和</show_render>。
12
+
13
+ # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-==-=-=-=/1=-=-=-=-=-=-=-=-=-=-=-=-=-=/2-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
14
+ const_extract_re = re.compile(
15
+ r"<gpt_academic_string_mask><show_llm>(.*?)</show_llm><show_render>(.*?)</show_render></gpt_academic_string_mask>"
16
+ )
17
+ # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-==-=-=-=-=-=/1=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-/2-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
18
+ const_extract_langbased_re = re.compile(
19
+ r"<gpt_academic_string_mask><lang_english>(.*?)</lang_english><lang_chinese>(.*?)</lang_chinese></gpt_academic_string_mask>",
20
+ flags=re.DOTALL,
21
+ )
22
+
23
+ @lru_cache(maxsize=128)
24
+ def apply_gpt_academic_string_mask(string, mode="show_all"):
25
+ """
26
+ 当字符串中有掩码tag时(<gpt_academic_string_mask><show_...>),根据字符串要给谁看(大模型,还是web渲染),对字符串进行处理,返回处理后的字符串
27
+ 示意图:https://mermaid.live/edit#pako:eNqlkUtLw0AUhf9KuOta0iaTplkIPlpduFJwoZEwJGNbzItpita2O6tF8QGKogXFtwu7cSHiq3-mk_oznFR8IYLgrGbuOd9hDrcCpmcR0GDW9ubNPKaBMDauuwI_A9M6YN-3y0bODwxsYos4BdMoBrTg5gwHF-d0mBH6-vqFQe58ed5m9XPW2uteX3Tubrj0ljLYcwxxR3h1zB43WeMs3G19yEM9uapDMe_NG9i2dagKw1Fee4c1D9nGEbtc-5n6HbNtJ8IyHOs8tbs7V2HrlDX2w2Y7XD_5haHEtQiNsOwfMVa_7TzsvrWIuJGo02qTrdwLk9gukQylHv3Afv1ML270s-HZUndrmW1tdA-WfvbM_jMFYuAQ6uCCxVdciTJ1CPLEITpo_GphypeouzXuw6XAmyi7JmgBLZEYlHwLB2S4gHMUO-9DH7tTnvf1CVoFFkBLSOk4QmlRTqpIlaWUHINyNFXjaQWpCYRURUKiWovBYo8X4ymEJFlECQUpqaQkJmuvWygPpg
28
+ """
29
+ if not string:
30
+ return string
31
+ if "<gpt_academic_string_mask>" not in string: # No need to process
32
+ return string
33
+
34
+ if mode == "show_all":
35
+ return string
36
+ if mode == "show_llm":
37
+ string = const_extract_re.sub(r"\1", string)
38
+ elif mode == "show_render":
39
+ string = const_extract_re.sub(r"\2", string)
40
+ else:
41
+ raise ValueError("Invalid mode")
42
+ return string
43
+
44
+
45
+ @lru_cache(maxsize=128)
46
+ def build_gpt_academic_masked_string(text_show_llm="", text_show_render=""):
47
+ """
48
+ 根据字符串要给谁看(大模型,还是web渲染),生成带掩码tag的字符串
49
+ """
50
+ return f"<gpt_academic_string_mask><show_llm>{text_show_llm}</show_llm><show_render>{text_show_render}</show_render></gpt_academic_string_mask>"
51
+
52
+
53
+ @lru_cache(maxsize=128)
54
+ def apply_gpt_academic_string_mask_langbased(string, lang_reference):
55
+ """
56
+ 当字符串中有掩码tag时(<gpt_academic_string_mask><lang_...>),根据语言,选择提示词,对字符串进行处理,返回处理后的字符串
57
+ 例如,如果lang_reference是英文,那么就只显示英文提示词,中文提示词就不显示了
58
+ 举例:
59
+ 输入1
60
+ string = "注意,lang_reference这段文字是:<gpt_academic_string_mask><lang_english>英语</lang_english><lang_chinese>中文</lang_chinese></gpt_academic_string_mask>"
61
+ lang_reference = "hello world"
62
+ 输出1
63
+ "注意,lang_reference这段文字是:英语"
64
+
65
+ 输入2
66
+ string = "注意,lang_reference这段文字是中文" # 注意这里没有掩码tag,所以不会被处理
67
+ lang_reference = "hello world"
68
+ 输出2
69
+ "注意,lang_reference这段文字是中文" # 原样返回
70
+ """
71
+
72
+ if "<gpt_academic_string_mask>" not in string: # No need to process
73
+ return string
74
+
75
+ def contains_chinese(string):
76
+ chinese_regex = re.compile(u'[\u4e00-\u9fff]+')
77
+ return chinese_regex.search(string) is not None
78
+
79
+ mode = "english" if not contains_chinese(lang_reference) else "chinese"
80
+ if mode == "english":
81
+ string = const_extract_langbased_re.sub(r"\1", string)
82
+ elif mode == "chinese":
83
+ string = const_extract_langbased_re.sub(r"\2", string)
84
+ else:
85
+ raise ValueError("Invalid mode")
86
+ return string
87
+
88
+
89
+ @lru_cache(maxsize=128)
90
+ def build_gpt_academic_masked_string_langbased(text_show_english="", text_show_chinese=""):
91
+ """
92
+ 根据语言,选择提示词,对字符串进行处理,返回处理后的字符串
93
+ """
94
+ return f"<gpt_academic_string_mask><lang_english>{text_show_english}</lang_english><lang_chinese>{text_show_chinese}</lang_chinese></gpt_academic_string_mask>"
95
+
96
+
97
+ if __name__ == "__main__":
98
+ # Test
99
+ input_string = (
100
+ "你好\n"
101
+ + build_gpt_academic_masked_string(text_show_llm="mermaid", text_show_render="")
102
+ + "你好\n"
103
+ )
104
+ print(
105
+ apply_gpt_academic_string_mask(input_string, "show_llm")
106
+ ) # Should print the strings with 'abc' in place of the academic mask tags
107
+ print(
108
+ apply_gpt_academic_string_mask(input_string, "show_render")
109
+ ) # Should print the strings with 'xyz' in place of the academic mask tags