console-player 1.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.2
2
+ Name: console-player
3
+ Version: 1.0
4
+ Summary: 让终端可以播放视频!
5
+ Home-page: https://github.com/SystemFileB/console-player
6
+ Author: SystemFileB
7
+ License: LGPLv3
8
+ Platform: Windows
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: py7zr
13
+ Requires-Dist: tqdm
14
+ Requires-Dist: Pillow
15
+ Requires-Dist: colorama
16
+ Requires-Dist: pygame
17
+ Dynamic: author
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: license
23
+ Dynamic: platform
24
+ Dynamic: requires-dist
25
+ Dynamic: summary
26
+
27
+ # 🎞️ console-player
28
+ 视频播放器,但终端
29
+
30
+ ## 📦 安装
31
+ ```bash
32
+ # Windows用户没有安装FFmpeg的,或者使用其他系统的
33
+ $ pip install console-player
34
+ # Windows用户安装了FFmpeg的
35
+ $ pip install console-player-noffmpeg
36
+ ```
37
+
38
+ ## ❓ 用法
39
+ ```bash
40
+ # 播放视频
41
+ $ consoleplay <CPVID文件>
42
+
43
+ # 生成CPVID文件
44
+ $ cpvgen # 交互生成
45
+ $ cpvgen <视频文件> <输出的CPVID文件,后缀必须是(.cpv;.cpvt;.zip)的任意一项> # 不让你选择文件的交互生成
46
+
47
+ # 在终端显示图片
48
+ $ consolepic <图片文件>
49
+
50
+ # 显示版本信息 (三选一)
51
+ $ consoleplay
52
+ $ consolepic
53
+ ```
54
+
55
+ ## ⚙️ API用法
56
+ ```python
57
+ from consoleplay import RGB,pic2terminal
58
+ from colorama import Style,init
59
+ init()
60
+
61
+
62
+ # 将图片打印到终端
63
+ pic2terminal("图片文件路径")
64
+
65
+ # 打印RGB颜色的字体
66
+ print(RGB(255,0,0)+"红色字体"+Style.RESET_ALL)
67
+ ```
68
+
69
+ ## 🔨 构建
70
+ ```bash
71
+ # Windows带FFmpeg
72
+ $ python setup.py bdist_wheel --have-ffmpeg
73
+
74
+ # Windows不带FFmpeg (注意包名变成了console-player-noffmpeg)
75
+ $ python setup.py bdist_wheel
76
+
77
+ # Linux
78
+ $ python setup.py bdist_wheel --linux
79
+
80
+ # MacOS
81
+ $ python setup.py bdist_wheel --mac
82
+ ```
83
+
84
+ ## 🛠️ CPVID文件的手工生成
85
+ 首先,你要知道CPVID本质上其实就是7z文件,只是后缀不同罢了
86
+ 知道了这个特性,然后创建一个新目录,并以这个结构创建文件(夹)
87
+
88
+ ```
89
+ 你的目录
90
+ |-- manifest.json
91
+ |-- audio.mp3
92
+ |-- frames
93
+ | |-- 1.jpg
94
+ | |-- 2.jpg
95
+ | |-- ...
96
+ | |-- n.jpg
97
+ ```
98
+
99
+ 如果你要打包CPVT格式的文件的话,请你把要输出的文字放入`frames/n.txt`中 (或使用xz压缩后使用`frames/n.txt.xz`) ,以替换`franes/n.jpg`中的图片
100
+
101
+ 然后,填充`audio.mp3`为你的音频
102
+
103
+ 接着,往`manifest.json`以这个格式写入内容
104
+ ```json
105
+ {
106
+ "frames": 视频帧数,填入数字,比如3500,
107
+ "fps": 视频的帧率,填入数字,要和frames和音频长度吻合,否则播放报错,比如20,
108
+ "type": 视频类型,比如"cpvt"或"cpv",
109
+ "xz": 你的cpvt类型的文件是否使用了xz压缩,填true或false,
110
+ "height": 视频高度,暂时没有实现,比如60
111
+ }
112
+ ```
113
+
114
+ 最后,把这个目录的所有内容压缩到7z文件,把后缀一改,搞定!
115
+
116
+ ## 📝 更新日志
117
+ ### 1.00
118
+ - 第一次发布
@@ -0,0 +1,12 @@
1
+ console_player_tools/ffmpeg.exe,sha256=G82xFiUZKJb4aTE9ONHjpUNeoT_UnmjpEqMx5DV6edQ,35514368
2
+ consolepic/__main__.py,sha256=ovDkM6_LBHuJX-0vUdgIdZ3hThnqlYzqpRHfgQWnbpg,770
3
+ consoleplay/__init__.py,sha256=SQ9DG3nLc_ywWXrpbS-12HLKd8qNGGJ0TUvqWJtRND0,4118
4
+ consoleplay/__main__.py,sha256=ixQOP7iUXTWOtMNO0_-zOa7iY_9WeKB1qHlQcWp_H8c,4400
5
+ consoleplay/icon.png,sha256=fskl8fF2FzcikXtIhasuhE3UWXlxRbo0sSPn2w8PTk4,24020
6
+ cpvgen/__main__.py,sha256=BxtqkaVv7NO4GWvXwlfKIJ6uq4-LNLN2sHNbDPKtKj4,6943
7
+ cpvgen/gen.py,sha256=uCzurIbzG3mLZxD4HJm3sIxgvwR-Bry-EpSID504M3M,9231
8
+ console_player-1.0.dist-info/METADATA,sha256=SEERviQ47YSdScATcP9xV-KeZm5RJv_b6OL7ERCUNxc,3056
9
+ console_player-1.0.dist-info/WHEEL,sha256=IS7GGfNNwN5x0ehvJ3YrlYCb3DoCm5wnLTXXYlwtEbw,97
10
+ console_player-1.0.dist-info/entry_points.txt,sha256=JFq_JtWMgJz7UjaWu82hQRNEBDxTeMH_LtUQM1lgBg4,125
11
+ console_player-1.0.dist-info/top_level.txt,sha256=UNunWBvi7nsAPiA5L3lgv3AoLexLSopKCjnP-_Pj11k,51
12
+ console_player-1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-win_amd64
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ consolepic = consolepic.__main__:main
3
+ consoleplay = consoleplay.__main__:main
4
+ cpvgen = cpvgen.__main__:run
@@ -0,0 +1,4 @@
1
+ console_player_tools
2
+ consolepic
3
+ consoleplay
4
+ cpvgen
Binary file
consolepic/__main__.py ADDED
@@ -0,0 +1,16 @@
1
+ import consoleplay as cp,sys,os
2
+ __version__=cp.version
3
+ def main():
4
+ if len(sys.argv)>1:
5
+ print(cp.pic2terminal(sys.argv[1]),try_rgba=True)
6
+ else:
7
+ path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8
+ versionstr=cp.pic2terminal(os.path.join(path,"consoleplay","icon.png"),th=19,try_rgba=True).split("\n")
9
+ versionstr[0]+=f" ConsolePlay {cp.version}"
10
+ versionstr[1]+=" By: SystemFileB和其他贡献者们"
11
+ versionstr[2]+=" 赞助: https://afdian.com/a/systemfileb"
12
+ versionstr[3]+=" Github: https://github.com/SystemFileB/console-player"
13
+ versionstr[4]+=" 用法: python -m consolepic <图片路径>"
14
+ print("\n".join(versionstr))
15
+ if __name__=="__main__":
16
+ main()
@@ -0,0 +1,100 @@
1
+ import shutil
2
+ from colorama import Style,Cursor,init
3
+ import lzma,os
4
+ from PIL import Image
5
+ from typing import IO, Union
6
+ StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]]
7
+
8
+ version="1.00"
9
+ __version__=version
10
+
11
+ tsize=shutil.get_terminal_size()
12
+ init()
13
+
14
+ def RGB(r, g, b, a=255):
15
+ """在要输出的文本前面加上这个,让文本的颜色变成对应的RGBA~"""
16
+ if a == 0:
17
+ return "\033[0m" # 重置所有样式(模拟完全透明)
18
+ elif a < 255:
19
+ # 混合到黑色背景的透明度效果(示例实现)
20
+ blend_factor = a / 255
21
+ r = int(r * blend_factor)
22
+ g = int(g * blend_factor)
23
+ b = int(b * blend_factor)
24
+ return f"\033[48;2;{r};{g};{b}m"
25
+
26
+ def pic2terminal(frame, lore=False,cursor_move=False,th=None,try_rgba=False):
27
+ """将图片转化为终端可显示的字符串,只支持RGB而不支持带透明度的图片
28
+ ### 参数
29
+ - frame: 图片路径,可以是Pillow支持的任何格式
30
+ - lore: 是否在移动光标的时候往上多移动一行
31
+ - cursor_move: 是否移动光标
32
+ - th: 生成的图片高度,如果没有指定就按照终端高度生成
33
+ - try_rgba: 是否尝试使用RGBA模式,如果失败就使用RGB模式
34
+ ### 用法
35
+ ```python
36
+ import consoleplay as cp
37
+ cp.pic2terminal("test.jpg")
38
+ ```
39
+ 这里仅包含了一般图片的转化,理论上io.BytesIO和bytes格式也可以"""
40
+ img = Image.open(frame)
41
+ # 转换为RGB模式来支持终端显示
42
+ if try_rgba and img.format in ("PNG", "WEBP", "TIFF"):
43
+ try:
44
+ img = img.convert('RGBA')
45
+ except:
46
+ img = img.convert('RGB')
47
+ else:
48
+ img = img.convert('RGB')
49
+
50
+ width, height = img.size
51
+ # 计算终端可用空间(宽度按字符数/2,高度保留3行给其他内容)
52
+ mode=not th
53
+ if mode:
54
+ tw, th = tsize # 获取完整终端尺寸
55
+ tw/=2 # 终端字符宽高比为1:2,每个像素用两个字符宽度表示
56
+ th-=3 # 保留底部3行用于控制台输出
57
+
58
+ # 根据终端空间计算最佳缩放比例
59
+ ratio = tw/width if mode and tw < th else th/height # 取最小缩放比例
60
+ width = int(width*ratio)
61
+ height = int(height*ratio)
62
+
63
+ # 使用最近邻算法快速缩放(保持锐利边缘)
64
+ img = img.resize((width, height),Image.Resampling.NEAREST)
65
+ print_str = ""
66
+ # 逐像素构建终端字符串
67
+ for y in range(height):
68
+ for x in range(width):
69
+ # 每个像素用两个空格字符表示(保持正确宽高比)
70
+ if img.mode == "RGBA":
71
+ r, g, b, a = img.getpixel((x, y))
72
+ else:
73
+ r, g, b = img.getpixel((x, y))
74
+ a=255
75
+ print_str+=RGB(r, g, b, a) + " "+Style.RESET_ALL
76
+ print_str+="\n" # 行结束换行
77
+
78
+ # 根据参数添加光标控制序列
79
+ if not cursor_move:
80
+ return print_str
81
+ # 计算光标回退位置(lore模式多回退一行)
82
+ return print_str + Cursor.UP(height+1 if lore else height) + Cursor.BACK(width*2)
83
+
84
+ def process_frame(frame_path: StrOrBytesPath | IO[bytes],text=False,th=None,xz=False):
85
+ """给consoleplay的cli使用的,最好不要在其他地方调用这个函数
86
+ ### 参数
87
+ 如果你确实想用的话,你可以这样调用:
88
+ frame_path: Pillow支持的图片格式,比如文件路径,io.BytesIO什么的
89
+ text: 是否直接将frame_path的部分解码并返回,只支持io.BytesIO格式,且文件编码必须是UTF-8
90
+ th: 终端的高度,可以不指定"""
91
+ if text:
92
+ if xz:
93
+ return lzma.decompress(frame_path.read(),format=lzma.FORMAT_XZ).decode("utf-8")
94
+ else:
95
+ return frame_path.read().decode("utf-8")
96
+ else:
97
+ if xz:
98
+ return lzma.compress(pic2terminal(frame_path, True, True,th).encode("utf-8"),format=lzma.FORMAT_XZ)
99
+ else:
100
+ return pic2terminal(frame_path, True, True,th)
@@ -0,0 +1,118 @@
1
+ import sys, os, time, shutil, py7zr, json
2
+ from colorama import Fore, Style, Cursor, init
3
+ from concurrent.futures import ThreadPoolExecutor
4
+ from . import process_frame,pic2terminal,version
5
+
6
+ __version__=version
7
+
8
+ path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
9
+ tsize=shutil.get_terminal_size()
10
+ init(autoreset=True)
11
+
12
+ def clear_screen():
13
+ print("\033c", end="")
14
+
15
+ def main():
16
+ from tqdm import tqdm
17
+ if len(sys.argv) < 2:
18
+ versionstr=pic2terminal(os.path.join(path,"consoleplay","icon.png"),th=19,try_rgba=True).split("\n")
19
+ versionstr[0]+=f" ConsolePlay {version}"
20
+ versionstr[1]+=" By: SystemFileB和其他贡献者们"
21
+ versionstr[2]+=" 赞助: https://afdian.com/a/systemfileb"
22
+ versionstr[3]+=" Github: https://github.com/SystemFileB/console-player"
23
+ versionstr[4]+=" 用法: python -m consoleplay <视频路径> [-mw=<最大工作线程数>]"
24
+ print("\n".join(versionstr))
25
+ sys.exit(0)
26
+ if not os.path.exists(sys.argv[1]):
27
+ print(f"{Fore.RED}E{Style.RESET_ALL}: 视频不存在")
28
+ sys.exit(2)
29
+ from pygame import mixer
30
+ mixer.init()
31
+ max_workers = os.cpu_count()
32
+ if len(sys.argv) > 2:
33
+ if sys.argv[2].startswith("-mw="):
34
+ max_workers = int(sys.argv[2][4:])
35
+
36
+ cpvid=py7zr.SevenZipFile(sys.argv[1], 'r')
37
+ print("读取所有文件...")
38
+ files=cpvid.readall()
39
+ manifest=json.load(files["manifest.json"])
40
+ del files["manifest.json"]
41
+ framesCount=manifest["frames"]
42
+ fps=manifest["fps"]
43
+ print(f"视频信息: 长度: {framesCount} FPS: {fps}")
44
+ print("加载音频文件...")
45
+ try:
46
+ audio=os.path.join(os.path.dirname(os.path.abspath(__file__)),"audio.mp3")
47
+ with open(os.path.join(os.path.dirname(os.path.abspath(__file__)),"audio.mp3"),"wb") as f:
48
+ f.write(files["audio.mp3"].read())
49
+ f.close()
50
+ except:
51
+ audio=os.path.join(os.path.expanduser("~"),"console_player.mp3")
52
+ with open(os.path.join(os.path.expanduser("~"),"console_player.mp3"),"wb") as f:
53
+ f.write(files["audio.mp3"].read())
54
+ f.close()
55
+ mixer.music.load(audio)
56
+ del files["audio.mp3"]
57
+
58
+ frames = []
59
+ cpvid.close()
60
+ cpvid=None
61
+ def processor(a):
62
+ fa=files[a]
63
+ del files[a]
64
+ return process_frame(fa,manifest["type"]=="cpvt",xz=manifest["xz"])
65
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
66
+ with tqdm(total=framesCount, unit='frame', desc="预处理所有帧", colour="green") as pbar:
67
+ if manifest["type"]=="cpvt":
68
+ if manifest["xz"]:
69
+ frame_paths = [f"frames/{i}.txt.xz" for i in range(1, framesCount + 1)]
70
+ else:
71
+ frame_paths = [f"frames/{i}.txt" for i in range(1, framesCount + 1)]
72
+ elif manifest["type"]=="cpv":
73
+ frame_paths = [files[f"frames/{i}.jpg"] for i in range(1, framesCount + 1)]
74
+ for result in executor.map(processor, frame_paths):
75
+ frames.append(result)
76
+ pbar.update(1)
77
+ files=None
78
+ clear_screen()
79
+ mixer.music.play()
80
+
81
+ i=0
82
+ old_i=0
83
+ while True:
84
+ try:
85
+ pos=mixer.music.get_pos()
86
+ i=pos//(1000//fps)
87
+ if i!=old_i:
88
+ if pos==-1 or i>=framesCount:
89
+ break
90
+ print(f"帧: {i}/{framesCount}\n"+frames[i],end="")
91
+ if i>=3:
92
+ frames[i-3]=None
93
+ old_i=i
94
+ time.sleep(0.00001)
95
+ except KeyboardInterrupt:
96
+ print(Cursor.UP(tsize.lines)+Cursor.BACK(tsize.columns),end="")
97
+ mixer.music.pause()
98
+ if input("已暂停,回车继续,'q'退出: ")=="q":
99
+ break
100
+ else:
101
+ clear_screen()
102
+ mixer.music.unpause()
103
+ frames=None
104
+ clear_screen()
105
+ print("清理音频文件...\n如果你在这里等待时间太久,你可以直接Ctrl+C")
106
+ mixer.music.stop()
107
+ mixer.quit()
108
+ while True:
109
+ try:
110
+ os.remove(audio)
111
+ except:
112
+ time.sleep(0.1)
113
+ else:
114
+ break
115
+ input("播放完毕,按下回车键退出")
116
+
117
+ if __name__ == "__main__":
118
+ main()
consoleplay/icon.png ADDED
Binary file
cpvgen/__main__.py ADDED
@@ -0,0 +1,175 @@
1
+ import sys, os,asyncio, shutil, json
2
+ from consoleplay import version
3
+ __version__=version
4
+ cli=False
5
+ try:
6
+ import easygui
7
+ except:
8
+ cli=True
9
+ if os.environ.get("CP_CLI"):
10
+ cli=True
11
+
12
+ async def main():
13
+ # 定义变量
14
+ if len(sys.argv) == 3:
15
+ inputVideo = sys.argv[1]
16
+ outputCPVid = sys.argv[2]
17
+ else:
18
+ inputVideo = ""
19
+ outputCPVid = ""
20
+ if len(sys.argv) == 4 and sys.argv[3].startswith("-mw="):
21
+ max_workers = int(sys.argv[3][4:])
22
+ elif len(sys.argv) == 2 and sys.argv[1].startswith("-mw="):
23
+ max_workers = int(sys.argv[1][4:])
24
+ else:
25
+ max_workers = os.cpu_count()
26
+ print("最大工作线程数:", max_workers)
27
+ if len(sys.argv) == 1:
28
+ print("CLI用法:python -m cpvgen <输入的视频> <cpv/cpvt/zip保存路径> [-mw=最大工作线程,仅限cpvt文件的保存]")
29
+ path = os.path.dirname(os.path.abspath(__file__))
30
+ mode=False
31
+ h=480
32
+ fps=20
33
+ try:
34
+ temp = os.path.join(path, "temp")
35
+ if not os.path.exists(temp):
36
+ os.makedirs(temp, exist_ok=True)
37
+ except:
38
+ if os.name == "nt":
39
+ temp = os.path.join(os.path.expanduser("~"), "AppData", "Local", "SystemFileB", "cpvid_generator")
40
+ if not os.path.exists(temp):
41
+ os.makedirs(temp, exist_ok=True)
42
+ else:
43
+ temp = os.path.join(os.path.expanduser("~"), ".SystemFileB", "cpvid_generator")
44
+ if not os.path.exists(temp):
45
+ os.makedirs(temp, exist_ok=True)
46
+ if os.path.exists(os.path.join(temp,"process.json")):
47
+ if cli:
48
+ inp=input("检测到上次未完成的任务,是否继续? (Y/n)")=="y"
49
+ else:
50
+ inp=easygui.ynbox("检测到上次未完成的任务,是否继续?")
51
+ if inp:
52
+ with open(os.path.join(temp,"process.json"),"r",encoding="utf-8") as f:
53
+ process_json=json.load(f)
54
+ inputVideo=process_json["argments"]["input"]
55
+ outputCPVid=process_json["argments"]["output"]
56
+ mode=process_json["argments"]["mode"]
57
+ h=process_json["argments"]["height"]
58
+ fps=process_json["argments"]["fps"]
59
+ temp=process_json["argments"]["temp"]
60
+ max_workers=process_json["argments"]["max_workers"]
61
+ xz=process_json["argments"]["xz"]
62
+ f.close()
63
+ from .gen import gen
64
+ await gen(inputVideo, outputCPVid, mode, h, fps, temp, max_workers, xz)
65
+ easygui.msgbox("完成","cpvid生成器")
66
+ return 0
67
+
68
+
69
+ if inputVideo == "" or outputCPVid == "":
70
+ if cli:
71
+ inputVideo = input("输入视频路径:")
72
+ outputCPVid = input("输入(cpv,cpvt,zip)文件保存路径:")
73
+ if not (inputVideo and outputCPVid):
74
+ print("输入不能为空")
75
+ return 1
76
+
77
+
78
+ else:
79
+ inputVideo = easygui.fileopenbox("请选择视频文件", "选择视频文件", filetypes=[["*.mp4;*.avi;*.mkv;*.flv;*.mov;*.wmv;*.ts;*.webm","FFmpeg支持的文件 (*.mp4;*.avi;*.mkv;*.flv;*.mov;*.wmv;*.ts;*.webm)"],
80
+ ["*.*","所有文件 (*.*)"]])
81
+ if not inputVideo:
82
+ return 0
83
+ outputCPVid = easygui.filesavebox("请选择cpvid文件保存路径", "选择cpvid文件保存路径", filetypes=[["*.cpv","保存为jpg图片的,可随终端拉伸的ConsolePlay文件 (*.cpv)"],
84
+ ["*.cpvt","保存为txt文本的,不可拉伸但加载快的ConsolePlayText文件 (*.cpvt)"],
85
+ ["*.zip","可在Minecraft播放的数据包和资源包文件 (未完工, *.zip)"],
86
+ ["*.*","所有文件 (*.*)"]
87
+ ], default="output.cpvt")
88
+ if not outputCPVid:
89
+ return 1
90
+ if outputCPVid.endswith(".cpv"):
91
+ mode="cpv"
92
+ h=480
93
+ elif outputCPVid.endswith(".cpvt"):
94
+ mode="cpvt"
95
+ h=shutil.get_terminal_size().lines
96
+ elif outputCPVid.endswith(".zip"):
97
+ mode="dp"
98
+ h=64
99
+ else:
100
+ mode="cpv"
101
+ if cli:
102
+ inp=input("视频高度({})".format(h))
103
+ if inp:
104
+ try:
105
+ h=int(inp)
106
+ except:
107
+ print("输入错误")
108
+ return 1
109
+ inp=input("视频帧率({})".format(fps))
110
+ if inp:
111
+ try:
112
+ fps=int(inp)
113
+ except:
114
+ print("输入错误")
115
+ return 1
116
+ else:
117
+ inp=easygui.enterbox("视频高度({})".format(h))
118
+ if inp:
119
+ try:
120
+ h=int(inp)
121
+ except:
122
+ easygui.msgbox("输入错误")
123
+ return 1
124
+ inp=easygui.enterbox("视频帧率({})".format(fps))
125
+ if inp:
126
+ try:
127
+ fps=int(inp)
128
+ except:
129
+ easygui.msgbox("输入错误")
130
+ return 1
131
+ xz=False
132
+ if mode=="cpvt":
133
+ if h>=150:
134
+ text=("你给的视频高度太大,可能会导致播放时卡顿 (除非你的终端性能很好)\n如果你愿意的话,可以打开帧的XZ压缩,让每个帧文件变小\n要压缩每一个帧吗 (Y/n):",True)
135
+ else:
136
+ text=("你要不要为每一个帧使用XZ压缩 (y/N):",False)
137
+
138
+ if cli:
139
+ inp=input(text[0])
140
+ else:
141
+ inp=easygui.ynbox(text[0])
142
+ if inp==None:
143
+ inp=""
144
+ elif inp:
145
+ inp="y"
146
+ else:
147
+ inp="n"
148
+
149
+ if inp=="Y" or inp=="y":
150
+ xz=True
151
+ elif inp=="N" or inp=="n":
152
+ xz=False
153
+ else:
154
+ xz=text[1]
155
+ if os.path.exists(outputCPVid):
156
+ if cli:
157
+ if input("文件已存在,是否覆盖并继续? (y/N):")=="y":
158
+ os.remove(outputCPVid)
159
+ else:
160
+ return 0
161
+ else:
162
+ if easygui.ynbox("文件已存在,是否覆盖并继续?"):
163
+ os.remove(outputCPVid)
164
+ else:
165
+ return 0
166
+ from .gen import gen
167
+ await gen(inputVideo, outputCPVid, mode, h, fps, temp, max_workers, xz)
168
+ easygui.msgbox("完成","cpvid生成器")
169
+ return 0
170
+
171
+ def run():
172
+ sys.exit(asyncio.run(main()))
173
+
174
+ if __name__ == "__main__":
175
+ run()
cpvgen/gen.py ADDED
@@ -0,0 +1,219 @@
1
+ import subprocess as sp, os, shutil, py7zr, asyncio, json
2
+ from tqdm import tqdm
3
+ from concurrent.futures import ThreadPoolExecutor
4
+ from consoleplay import process_frame
5
+ manifest={}
6
+
7
+ async def rm(temp):
8
+ files = [os.path.join(temp, filename) for filename in os.listdir(temp)]
9
+ if files:
10
+ with tqdm(total=len(files), unit='file', desc="删除文件", colour="green") as pbar:
11
+ for file_path in files:
12
+ try:
13
+ if os.path.isfile(file_path) or os.path.islink(file_path):
14
+ os.unlink(file_path)
15
+ elif os.path.isdir(file_path):
16
+ shutil.rmtree(file_path)
17
+ pbar.update(1)
18
+ except Exception as e:
19
+ print(f'无法删除{file_path} {e}')
20
+
21
+ async def compress_directory(directory_path, output_path):
22
+ # 修正后的文件计数逻辑,排除 process.json
23
+ total_files = sum(
24
+ len([f for f in files if f != "process.json"])
25
+ for _, _, files in os.walk(directory_path)
26
+ )
27
+
28
+ with py7zr.SevenZipFile(output_path, 'w', mp=True) as archive:
29
+ with tqdm(total=total_files, unit='file', desc="压缩文件",colour="green") as pbar:
30
+ for root, dirs, files in os.walk(directory_path):
31
+ for file in files:
32
+ if file == "process.json": # 跳过 process.json
33
+ continue
34
+ file_path = os.path.join(root, file)
35
+ archive.write(file_path, os.path.relpath(file_path, directory_path))
36
+ pbar.update(1)
37
+ async def ffmpeg_processer(input,temp,height,fps,mode) -> bool:
38
+ ffmpeg=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))+"/console_player_tools/ffmpeg.exe"
39
+ if not os.path.exists(ffmpeg):
40
+ ffmpeg="ffmpeg"
41
+
42
+ os.makedirs(os.path.join(temp,"frames"),exist_ok=True)
43
+ ffmpeg_frame = [
44
+ ffmpeg,
45
+ '-i', input,
46
+ '-vf', f'scale=-1:{height},fps={fps}',
47
+ '-hide_banner',
48
+ f'{temp}/frames/%d.jpg'
49
+ ]
50
+ if mode=="dp":
51
+ ffmpeg_audio = [
52
+ ffmpeg,
53
+ '-i', input,
54
+ '-vn',
55
+ '-hide_banner',
56
+ f'{temp}/audio.ogg'
57
+ ]
58
+ else:
59
+ ffmpeg_audio = [
60
+ ffmpeg,
61
+ '-i', input,
62
+ '-vn',
63
+ '-hide_banner',
64
+ f'{temp}/audio.mp3'
65
+ ]
66
+ audio_runner=sp.run(ffmpeg_audio) # 如果你在这里报错,请安装ffmpeg并放在path里
67
+ if audio_runner.returncode!=0:
68
+ return False
69
+ frame_runner=sp.run(ffmpeg_frame)
70
+ if frame_runner.returncode!=0:
71
+ return False
72
+ return True
73
+
74
+ async def write_manifest(temp,mode,height,fps,xz):
75
+ global manifest
76
+ print("写信息文件...")
77
+ with open(os.path.join(temp,"manifest.json"),"w") as f:
78
+ manifest={
79
+ "frames":len(os.listdir(os.path.join(temp,"frames"))),
80
+ "fps":fps,
81
+ "type":mode,
82
+ "xz":xz
83
+ }
84
+ if mode=="cpvt":
85
+ manifest["height"]=height
86
+ json.dump(manifest,f)
87
+ f.close()
88
+
89
+ async def pre_gen_frames(mode,max_workers,manifest,temp,height,xz,process_json,file_count=1):
90
+ if mode=="dp":
91
+ pass
92
+ #os.makedirs(os.path.join(temp,"datapack","data"),exist_ok=True)
93
+ elif mode=="cpvt":
94
+ frames = []
95
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
96
+ with tqdm(total=manifest["frames"], unit='frame', desc="将帧转为终端文字", colour="green") as pbar:
97
+ # 修改路径生成方式,使用os.path处理路径
98
+ frame_paths = [os.path.join(temp, "frames", f"{i}.jpg") for i in range(file_count, manifest["frames"] + 1)]
99
+ if file_count!=1:
100
+ print("从倒下的地方再站起来!已处理的帧数量:",file_count)
101
+ pbar.update(file_count)
102
+ file_index=file_count
103
+ else:
104
+ file_index=1
105
+
106
+
107
+ # 分批处理帧数据,每批50个任务
108
+ chunk_size = 50
109
+ futures = []
110
+ for i, path in enumerate(frame_paths):
111
+ futures.append(executor.submit(process_frame, path, th=height, xz=xz))
112
+ # 每积累一定数量的任务就等待完成
113
+ if (i + 1) % chunk_size == 0:
114
+ for future, frame_path in zip(futures, frame_paths[i-chunk_size+1:i+1]):
115
+ result = future.result()
116
+ if xz:
117
+ txt_path = os.path.splitext(frame_path)[0] + ".txt.xz"
118
+ with open(txt_path, "wb") as f:
119
+ f.write(result)
120
+ f.close()
121
+ else:
122
+ txt_path = os.path.splitext(frame_path)[0] + ".txt"
123
+ with open(txt_path, "w", encoding="utf-8") as f:
124
+ f.write(result)
125
+ f.close()
126
+ os.unlink(frame_path)
127
+
128
+ file_index+=1
129
+ pbar.update(1)
130
+
131
+ #with open(process_json,"r+",encoding="utf-8") as f:
132
+ # process_json2=json.load(f)
133
+ # process_json2["step3_processCount"]=file_index
134
+ # f.seek(0)
135
+ # json.dump(process_json2,f,indent=4,ensure_ascii=False)
136
+ # f.truncate()
137
+ # f.close()
138
+ futures = []
139
+ await asyncio.sleep(0) # 释放事件循环
140
+
141
+ # 处理剩余任务
142
+ for future, frame_path in zip(futures, frame_paths[-len(futures):]):
143
+ result = future.result()
144
+ txt_path = os.path.splitext(frame_path)[0] + ".txt"
145
+ if xz:
146
+ txt_path = os.path.splitext(frame_path)[0] + ".txt.xz"
147
+ with open(txt_path, "wb") as f:
148
+ f.write(result)
149
+ f.close()
150
+ else:
151
+ txt_path = os.path.splitext(frame_path)[0] + ".txt"
152
+ with open(txt_path, "w", encoding="utf-8") as f:
153
+ f.write(result)
154
+ f.close()
155
+ os.unlink(frame_path)
156
+ pbar.update(1)
157
+
158
+ # 先递增索引再保存进度
159
+ file_index += 1 # 移动到前面
160
+ # 使用更安全的写入方式
161
+ #with open(process_json, "r+", encoding="utf-8") as f:
162
+ # process_data = json.load(f)
163
+ # process_data["step3_processCount"] = file_index
164
+ # f.seek(0)
165
+ # json.dump(process_data, f, indent=4, ensure_ascii=False)
166
+ # f.truncate()
167
+ await asyncio.sleep(0.5)
168
+
169
+ async def gen(input,output,mode,height,fps,temp,max_workers=None,xz=False):
170
+ global manifest
171
+ # 初始化部分
172
+ if not os.path.exists(os.path.join(temp,"process.json")):
173
+ await rm(temp)
174
+ start_i=0
175
+ step3_processCount=1
176
+ process_json={
177
+ "step":0,
178
+ "step3_processCount":1,
179
+ "argments": {
180
+ "input":input,
181
+ "output":output,
182
+ "mode":mode,
183
+ "height":height,
184
+ "fps":fps,
185
+ "temp":temp,
186
+ "max_workers":max_workers,
187
+ "xz":xz
188
+ }
189
+ }
190
+ else:
191
+ with open(os.path.join(temp,"process.json"),"r",encoding="utf-8") as f:
192
+ process_json=json.load(f)
193
+ start_i=process_json["step"]
194
+ step3_processCount=process_json["step3_processCount"]
195
+ f.close()
196
+ if start_i>=2:
197
+ with open(os.path.join(temp,"manifest.json"),"r") as f:
198
+ print("读回manifest...")
199
+ manifest=json.load(f)
200
+ f.close()
201
+
202
+ for i in range(start_i,5):
203
+ if i==0:
204
+ await ffmpeg_processer(input,temp,height,fps,mode)
205
+ elif i==1:
206
+ await write_manifest(temp,mode,height,fps,xz)
207
+ elif i==2:
208
+ await pre_gen_frames(mode,max_workers,manifest,temp,height,xz,os.path.join(temp,"process.json"),step3_processCount)
209
+ elif i==3:
210
+ await compress_directory(temp,output)
211
+ elif i==4:
212
+ print("再次清空目录...")
213
+ await rm(temp)
214
+ await asyncio.sleep(0.5)
215
+ break
216
+ with open(os.path.join(temp,"process.json"),"w",encoding="utf-8") as f:
217
+ process_json["step"]=i
218
+ f.close()
219
+