File size: 21,360 Bytes
d5d20be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# -*- coding: utf-8 -*-

"""
@Time     : 2022/8/27 14:17
@Author   : cuny
@File     : app.py
@Software : PyCharm
@Introduce: 
查看包版本等一系列操作
"""
import os
import sys
import json
import shutil
import zipfile
import requests
from argparse import ArgumentParser
from importlib.metadata import version
try:  # 加上这个try的原因在于本地环境和云函数端的import形式有所不同
    from qcloud_cos import CosConfig
    from qcloud_cos import CosS3Client
except ImportError:
    try:
        from qcloud_cos_v5 import CosConfig
        from qcloud_cos_v5 import CosS3Client
        from qcloud_cos.cos_exception import CosServiceError
    except ImportError:
        raise ImportError("请下载腾讯云COS相关代码包:pip install cos-python-sdk-v5")


class HivisionaiParams(object):
    """
    定义一些基本常量
    """
    # 文件所在路径
    # 包名称
    package_name = "HY-sdk"
    # 腾讯云相关变量
    region = "ap-beijing"
    zip_key = "HY-sdk/"  # zip存储的云端文件夹路径,这里改了publish.yml也需要更改
    # 云端用户配置,如果在cloud_config_save不存在,就需要下载此文件
    user_url = "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/sdk-user/user_config.json"
    bucket = "cloud-public-static-1306602019"
    # 压缩包类型
    file_format = ".zip"
    # 下载路径(.hivisionai文件夹路径)
    download_path = os.path.expandvars('$HOME')
    # zip文件、zip解压缩文件的存放路径
    save_folder = f"{os.path.expandvars('$HOME')}/.hivisionai/sdk"
    # 腾讯云配置文件存放路径
    cloud_config_save = f"{os.path.expandvars('$HOME')}/.hivisionai/user_config.json"
    # 项目路径
    hivisionai_path = os.path.dirname(os.path.dirname(__file__))
    # 使用hivisionai的路径
    getcwd = os.getcwd()
    # HY-func的依赖配置
    # 每个依赖会包含三个参数,保存路径(save_path,相对于HY_func的路径)、下载url(url)
    functionDependence = {
        "configs":  [
            # --------- 配置文件部分
            # _lib
            {
                "url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_lib/config/aliyun-human-matting-api.json",
                "save_path": "_lib/config/aliyun-human-matting-api.json"
            },
            {
                "url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_lib/config/megvii-face-plus-api.json",
                "save_path": "_lib/config/megvii-face-plus-api.json"
            },
            {
                "url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_lib/config/volcano-face-change-api.json",
                "save_path": "_lib/config/volcano-face-change-api.json"
            },
            # _service
            {
                "url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_service/config/func_error_conf.json",
                "save_path": "_service/utils/config/func_error_conf.json"
            },
            {
                "url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_service/config/service_config.json",
                "save_path": "_service/utils/config/service_config.json"
            },
            # --------- 模型部分
            # 模型部分存储在Notion文档当中
            # https://www.notion.so/HY-func-cc6cc41ba6e94b36b8fa5f5d67d1683f
        ],
        "weights": "https://www.notion.so/HY-func-cc6cc41ba6e94b36b8fa5f5d67d1683f"
    }


class HivisionaiUtils(object):
    """
    本类为一些基本工具类,包含代码复用相关内容
    """
    @staticmethod
    def get_client():
        """获取cos客户端对象"""
        def get_secret():
            # 首先判断cloud_config_save下是否存在
            if not os.path.exists(HivisionaiParams.cloud_config_save):
                print("Downloading user_config...")
                resp = requests.get(HivisionaiParams.user_url)
                open(HivisionaiParams.cloud_config_save, "wb").write(resp.content)
            config = json.load(open(HivisionaiParams.cloud_config_save, "r"))
            return config["secret_id"], config["secret_key"]
        # todo 接入HY-Auth-Sync
        secret_id, secret_key = get_secret()
        return CosS3Client(CosConfig(Region=HivisionaiParams.region, Secret_id=secret_id, Secret_key=secret_key))

    def get_all_versions(self):
        """获取云端的所有版本号"""
        def getAllVersion_base():
            """
            返回cos存储桶内部的某个文件夹的内部名称
            ps:如果需要修改默认的存储桶配置,请在代码运行的时候加入代码 s.bucket = 存储桶名称 (s是对象实例)
            返回的内容存储在response["Content"],不过返回的数据大小是有限制的,具体内容还是请看官方文档。
            Returns:
                [版本列表]
            """
            resp = client.list_objects(
                Bucket=HivisionaiParams.bucket,
                Prefix=HivisionaiParams.zip_key,
                Marker=marker
            )
            versions_list.extend([x["Key"].split("/")[-1].split(HivisionaiParams.file_format)[0] for x in resp["Contents"] if int(x["Size"]) > 0])
            if resp['IsTruncated'] == 'false':  # 接下来没有数据了,就退出
                return ""
            else:
                return resp['NextMarker']
        client = self.get_client()
        marker = ""
        versions_list = []
        while True:  # 轮询
            try:
                marker = getAllVersion_base()
            except KeyError as e:
                print(e)
                raise
            if len(marker) == 0:  # 没有数据了
                break
        return versions_list

    def get_newest_version(self):
        """获取最新的版本号"""
        versions_list = self.get_all_versions()
        # reverse=True,降序
        versions_list.sort(key=lambda x: int(x.split(".")[-1]), reverse=True)
        versions_list.sort(key=lambda x: int(x.split(".")[-2]), reverse=True)
        versions_list.sort(key=lambda x: int(x.split(".")[-3]), reverse=True)
        return versions_list[0]

    def download_version(self, v):
        """
        在存储桶中下载文件,将下载好的文件解压至本地
        Args:
            v: 版本号,x.x.x

        Returns:
            None
        """
        file_name = v + HivisionaiParams.file_format
        client = self.get_client()
        print(f"Download to {HivisionaiParams.save_folder}...")
        try:
            resp = client.get_object(HivisionaiParams.bucket, HivisionaiParams.zip_key + "/" + file_name)
            contents = resp["Body"].get_raw_stream().read()
        except CosServiceError:
            print(f"[{file_name}.zip] does not exist, please check your version!")
            sys.exit()
        if not os.path.exists(HivisionaiParams.save_folder):
            os.makedirs(HivisionaiParams.save_folder)
        open(os.path.join(HivisionaiParams.save_folder, file_name), "wb").write(contents)
        print("Download success!")

    @staticmethod
    def download_dependence(path=None):
        """
        一键下载HY-sdk所需要的所有依赖,需要注意的是,本方法必须在运行pip install之后使用(运行完pip install之后才会出现hivisionai文件夹)
        Args:
            path: 文件路径,精确到hivisionai文件夹的上一个目录,如果为None,则默认下载到python环境下hivisionai安装的目录

        Returns:
            下载相应内容到指定位置
        """
        # print("指定的下载路径:", path)  # 此时在path路径下必然存在一个hivisionai文件夹
        # print("系统安装的hivisionai库的路径:", HivisionaiParams.hivisionai_path)
        print("Dependence downloading...")
        if path is None:
            path = HivisionaiParams.hivisionai_path
        # ----------------下载mtcnn模型文件
        mtcnn_path = os.path.join(path, "hivisionai/hycv/mtcnn_onnx/weights")
        base_url = "https://linimages.oss-cn-beijing.aliyuncs.com/"
        onnx_files = ["pnet.onnx",  "rnet.onnx", "onet.onnx"]
        print(f"Downloading mtcnn model in {mtcnn_path}")
        if not os.path.exists(mtcnn_path):
            os.mkdir(mtcnn_path)
        for onnx_file in onnx_files:
            if not os.path.exists(os.path.join(mtcnn_path, onnx_file)):
                # download onnx model
                onnx_url = base_url + onnx_file
                print("Downloading Onnx Model in:", onnx_url)
                r = requests.get(onnx_url, stream=True)
                if r.status_code == 200:
                    open(os.path.join(mtcnn_path, onnx_file), 'wb').write(r.content)  # 将内容写入文件
                    print(f"Download finished -- {onnx_file}")
                del r
        # ----------------
        print("Dependence download finished...")


class HivisionaiApps(object):
    """
    本类为app对外暴露的接口,为了代码规整性,这里使用类来对暴露接口进行调整
    """
    @staticmethod
    def show_cloud_version():
        """查看在cos中的所有HY-sdk版本"""
        print("Connect to COS...")
        versions_list = hivisionai_utils.get_all_versions()
        # reverse=True,降序
        versions_list.sort(key=lambda x: int(x.split(".")[-1]), reverse=True)
        versions_list.sort(key=lambda x: int(x.split(".")[-2]), reverse=True)
        versions_list.sort(key=lambda x: int(x.split(".")[-3]), reverse=True)
        if len(versions_list) == 0:
            print("There is no version currently, please release it first!")
            sys.exit()
        versions = "The currently existing versions (Keep 10): \n"
        for i, v in enumerate(versions_list):
            versions += str(v) + " "
            if i == 9:
                break
        print(versions)

    @staticmethod
    def upgrade(v: str, enforce: bool = False, save_cached: bool = False):
        """
        自动升级HY-sdk到指定版本
        Args:
            v: 指定的版本号,格式为x.x.x
            enforce: 是否需要强制执行更新命令
            save_cached: 是否保存下载的wheel文件,默认为否
        Returns:
            None
        """
        def check_format():
            # noinspection PyBroadException
            try:
                major, minor, patch = v.split(".")
                int(major)
                int(minor)
                int(patch)
            except Exception as e:
                print(f"Illegal version number!\n{e}")
            pass
        print("Upgrading, please wait a moment...")
        if v == "-1":
            v = hivisionai_utils.get_newest_version()
        # 检查format的格式
        check_format()
        if v == version(HivisionaiParams.package_name) and not enforce:
            print(f"Current version: {v} already exists, skip installation.")
            sys.exit()
        hivisionai_utils.download_version(v)
        # 下载完毕(下载至save_folder),解压文件
        target_zip = os.path.join(HivisionaiParams.save_folder, f"{v}.zip")
        assert zipfile.is_zipfile(target_zip), "Decompression failed, and the target was not a zip file."
        new_dir = target_zip.replace('.zip', '')  # 解压的文件名
        if os.path.exists(new_dir):  # 判断文件夹是否存在
            shutil.rmtree(new_dir)
        os.mkdir(new_dir)  # 新建文件夹
        f = zipfile.ZipFile(target_zip)
        f.extractall(new_dir)  # 提取zip文件
        print("Decompressed, begin to install...")
        os.system(f'pip3 install {os.path.join(new_dir, "**.whl")}')
        # 开始自动下载必要的模型依赖
        hivisionai_utils.download_dependence()
        # 安装完毕,如果save_cached为真,删除"$HOME/.hivisionai/sdk"内部的所有文件元素
        if save_cached is True:
            os.system(f'rm -rf {HivisionaiParams.save_folder}/**')

    @staticmethod
    def export(path):
        """
        输出最新版本的文件到命令运行的path目录
        Args:
            path: 用户输入的路径

        Returns:
            输出最新的hivisionai到path目录
        """
        # print(f"当前路径: {os.path.join(HivisionaiParams.getcwd, path)}")
        # print(f"文件路径: {os.path.dirname(__file__)}")
        export_path = os.path.join(HivisionaiParams.getcwd, path)
        # 判断输出路径存不存在,如果不存在,就报错
        assert os.path.exists(export_path), f"{export_path} dose not Exists!"
        v = hivisionai_utils.get_newest_version()
        # 下载文件到.hivisionai/sdk当中
        hivisionai_utils.download_version(v)
        # 下载完毕(下载至save_folder),解压文件
        target_zip = os.path.join(HivisionaiParams.save_folder, f"{v}.zip")
        assert zipfile.is_zipfile(target_zip), "Decompression failed, and the target was not a zip file."
        new_dir = os.path.basename(target_zip.replace('.zip', ''))  # 解压的文件名
        new_dir = os.path.join(export_path, new_dir)  # 解压的文件路径
        if os.path.exists(new_dir):  # 判断文件夹是否存在
            shutil.rmtree(new_dir)
        os.mkdir(new_dir)  # 新建文件夹
        f = zipfile.ZipFile(target_zip)
        f.extractall(new_dir)  # 提取zip文件
        print("Decompressed, begin to export...")
        # 强制删除bin/hivisionai和hivisionai/以及HY_sdk-**
        bin_path = os.path.join(export_path, "bin")
        hivisionai_path = os.path.join(export_path, "hivisionai")
        sdk_path = os.path.join(export_path, "HY_sdk-**")
        os.system(f"rm -rf {bin_path} {hivisionai_path} {sdk_path}")
        # 删除完毕,开始export
        os.system(f'pip3 install {os.path.join(new_dir, "**.whl")} -t {export_path}')
        hivisionai_utils.download_dependence(export_path)
        # 将下载下来的文件夹删除
        os.system(f'rm -rf {target_zip} && rm -rf {new_dir}')
        print("Done.")

    @staticmethod
    def hy_func_init(force):
        """
        在HY-func目录下使用hivisionai --init,可以自动将需要的依赖下载到指定位置
        不过对于比较大的模型——修复模型而言,需要手动下载
        Args:
            force: 如果force为True,则会强制重新下载所有的内容,包括修复模型这种比较大的模型
        Returns:
            程序执行完毕,会将一些必要的依赖也下载完毕
        """
        cwd = HivisionaiParams.getcwd
        # 判断当前文件夹是否是HY-func
        dirName = os.path.basename(cwd)
        assert dirName == "HY-func", "请在正确的文件目录下初始化HY-func!"
        # 需要下载的内容会存放在HivisionaiParams的functionDependence变量下
        functionDependence = HivisionaiParams.functionDependence
        # 下载配置文件
        configs = functionDependence["configs"]
        print("正在下载配置文件...")
        for config in configs:
            if not force and os.path.exists(config['save_path']):
                print(f"[pass]: {os.path.basename(config['url'])}")
                continue
            print(f"[Download]: {config['url']}")
            resp = requests.get(config['url'])
            # json文件存储在text区域,但是其他的不一定
            open(os.path.join(cwd, config['save_path']), 'w').write(resp.text)
        # 其他文件,提示访问notion文档
        print(f"[NOTICE]: 一切准备就绪,请访问下面的文档下载剩下的模型文件:\n{functionDependence['weights']}")

    @staticmethod
    def hy_func_deploy(functionName: str = None, functionPath: str = None):
        """
        在HY-func目录下使用此命令,并且随附功能函数的名称,就可以将HY-func的部署版放到桌面上
        但是需要注意的是,本方式不适合修复功能使用,修复功能依旧需要手动制作镜像
        Args:
            functionName: 功能函数名称
            functionPath: 需要注册的HY-func路径

        Returns:
            程序执行完毕,桌面会出现一个同名文件夹
        """
        # 为了代码撰写的方便,这里仅仅把模型文件删除,其余配置文件保留
        # 为了实现在任意位置输入hivisionai --deploy funcName都能成功,在使用前需要在.hivisionai/user_config.json中注册
        # print(functionName, functionPath)
        if functionPath is not None:
            # 更新/添加路径
            # functionPath为相对于使用路径的路径
            assert os.path.basename(functionPath) == "HY-func", "所指向路径非HY-func!"
            func_path = os.path.join(HivisionaiParams.getcwd, functionPath)
            assert os.path.join(func_path), f"路径不存在: {func_path}"
            # functionPath的路径写到user_config当中
            user_config = json.load(open(HivisionaiParams.cloud_config_save, 'rb'))
            user_config["func_path"] = func_path
            open(HivisionaiParams.cloud_config_save, 'w').write(json.dumps(user_config))
            print("HY-func全局路径保存成功!")
        try:
            user_config = json.load(open(HivisionaiParams.cloud_config_save, 'rb'))
            func_path = user_config['func_path']
        except KeyError:
            return print("请先使用-p命令注册全局HY-func路径!")
        # 此时func_path必然存在
        # print(os.listdir(func_path))
        assert functionName in os.listdir(func_path), functionName + "功能不存在!"
        func_path_deploy = os.path.join(func_path, functionName)
        # 开始复制文件到指定目录
        # 我们默认移动到Desktop目录下,如果没有此目录,需要先创建一个
        target_dir = os.path.join(HivisionaiParams.download_path, "Desktop")
        assert os.path.exists(target_dir), target_dir + "文件路径不存在,你需要先创建一下!"
        # 开始移动
        target_dir = os.path.join(target_dir, functionName)
        print("正在复制需要部署的文件...")
        os.system(f"rm -rf {target_dir}")
        os.system(f'cp -rf {func_path_deploy} {target_dir}')
        os.system(f"cp -rf {os.path.join(func_path, '_lib')} {target_dir}")
        os.system(f"cp -rf {os.path.join(func_path, '_service')} {target_dir}")
        # 生成最新的hivisionai
        print("正在生成hivisionai代码包...")
        os.system(f'hivisionai -t {target_dir}')
        # 移动完毕,删除模型文件
        print("移动完毕,正在删除不需要的文件...")
        # 模型文件
        os.system(f"rm -rf {os.path.join(target_dir, '_lib', 'weights', '**')}")
        # hivisionai生成时的多余文件
        os.system(f"rm -rf {os.path.join(target_dir, 'bin')} {os.path.join(target_dir, 'HY_sdk**')}")
        print("部署文件生成成功,你可以开始部署了!")


hivisionai_utils = HivisionaiUtils()


def entry_point():
    parser = ArgumentParser()
    # 查看版本号
    parser.add_argument("-v", "--version", action="store_true", help="View the current HY-sdk version, which does not represent the final cloud version.")
    # 自动更新
    parser.add_argument("-u", "--upgrade", nargs='?', const="-1", type=str, help="Automatically update HY-sdk to the latest version")
    # 查找云端的HY-sdk版本
    parser.add_argument("-l", "--list", action="store_true", help="Find HY-sdk versions of the cloud, and keep up to ten")
    # 下载云端的版本到本地路径
    parser.add_argument("-t", "--export", nargs='?', const="./", help="Add a path parameter to automatically download the latest version of sdk to this path. If there are no parameters, the default is the current path")
    # 强制更新附带参数,当一个功能需要强制执行一遍的时候,需要附带此参数
    parser.add_argument("-f", "--force", action="store_true", help="Enforcement of other functions, execution of a single parameter is meaningless")
    # 初始化HY-func
    parser.add_argument("--init", action="store_true", help="Initialization HY-func")
    # 部署HY-func
    parser.add_argument("-d", "--deploy", nargs='?', const="-1", type=str, help="Deploy HY-func")
    # 涉及注册一些自定义内容的时候,需要附带此参数,并写上自定义内容
    parser.add_argument("-p", "--param", nargs='?', const="-1", type=str, help="When registering some custom content, you need to attach this parameter and write the custom content.")
    args = parser.parse_args()
    if args.version:
        print(version(HivisionaiParams.package_name))
        sys.exit()
    if args.upgrade:
        HivisionaiApps.upgrade(args.upgrade, args.force)
        sys.exit()
    if args.list:
        HivisionaiApps.show_cloud_version()
        sys.exit()
    if args.export:
        HivisionaiApps.export(args.export)
        sys.exit()
    if args.init:
        HivisionaiApps.hy_func_init(args.force)
        sys.exit()
    if args.deploy:
        HivisionaiApps.hy_func_deploy(args.deploy, args.param)


if __name__ == "__main__":
    entry_point()