pipesay 0.0.1__tar.gz

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,70 @@
1
+ # This workflow will upload a Python Package to PyPI when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3
+
4
+ # This workflow uses actions that are not certified by GitHub.
5
+ # They are provided by a third-party and are governed by
6
+ # separate terms of service, privacy policy, and support
7
+ # documentation.
8
+
9
+ name: Upload Python Package
10
+
11
+ on:
12
+ release:
13
+ types: [published]
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ release-build:
20
+ runs-on: ubuntu-latest
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.x"
28
+
29
+ - name: Build release distributions
30
+ run: |
31
+ # NOTE: put your own distribution build steps here.
32
+ python -m pip install build
33
+ python -m build
34
+
35
+ - name: Upload distributions
36
+ uses: actions/upload-artifact@v4
37
+ with:
38
+ name: release-dists
39
+ path: dist/
40
+
41
+ pypi-publish:
42
+ runs-on: ubuntu-latest
43
+ needs:
44
+ - release-build
45
+ permissions:
46
+ # IMPORTANT: this permission is mandatory for trusted publishing
47
+ id-token: write
48
+
49
+ # Dedicated environments with protections for publishing are strongly recommended.
50
+ # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
51
+ environment:
52
+ name: pypi
53
+ # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
54
+ # url: https://pypi.org/p/YOURPROJECT
55
+ #
56
+ # ALTERNATIVE: if your GitHub Release name is the PyPI project version string
57
+ # ALTERNATIVE: exactly, uncomment the following line instead:
58
+ # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
59
+
60
+ steps:
61
+ - name: Retrieve release distributions
62
+ uses: actions/download-artifact@v4
63
+ with:
64
+ name: release-dists
65
+ path: dist/
66
+
67
+ - name: Publish release distributions to PyPI
68
+ uses: pypa/gh-action-pypi-publish@release/v1
69
+ with:
70
+ packages-dir: dist/
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+
5
+ .ruff_cache/
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+
9
+ .venv/
10
+ venv/
11
+ env/
12
+
13
+ .vscode/
14
+ .idea/
15
+
16
+ .test/
pipesay-0.0.1/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright © 2026 OrO-c
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
pipesay-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.5
2
+ Name: pipesay
3
+ Version: 0.0.1
4
+ Summary: 一个用管道处理句子的趣味输出工具
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: desktop-notifier
8
+ Requires-Dist: jieba
9
+ Requires-Dist: openai
10
+ Requires-Dist: pyaml
11
+ Requires-Dist: python-cowsay
12
+ Requires-Dist: requests
13
+ Requires-Dist: translators
14
+ Requires-Dist: urllib3
15
+ Description-Content-Type: text/markdown
16
+
17
+ # PipeSay
18
+
19
+ > 一个可插拔的「句子流水线」框架:抓取 → 加工 → 输出。
20
+
21
+ PipeSay 将句子处理抽象为一条可配置的流水线。每个环节都通过装饰器注册、通过 YAML 组合,你无需改动核心代码,就能拼装出自己的玩法。
22
+
23
+ ## 设计理念
24
+
25
+ - **三段式流水线**:`Fetcher` → `Processor` → `Outputer`,职责清晰,互不耦合。
26
+ - **注册式插件系统**:`@processor('name')` / `@outputer('name')` 一键注册,签名自动反射为可配置参数。
27
+ - **YAML 驱动**:处理器与输出器均以声明式配置组合,支持全局参数与步骤级参数合并。
28
+ - **面向扩展**:核心只提供约定与调度,具体能力全部来自插件。
29
+
30
+ ## 安装
31
+ ```bash
32
+ pip install -r requirements.txt
33
+ ```
34
+
35
+ ## 快速开始
36
+
37
+ 从网络获取一言并输出到终端:
38
+ ```bash
39
+ python -m src.pipesay.main hitokoto -o test2.yaml
40
+ ```
41
+
42
+ 从本地文件读取、加工后输出:
43
+ ```bash
44
+ python -m src.pipesay.main local -f ./cowsay.txt -s test.yaml -o test2.yaml
45
+ ```
46
+ ## 命令行
47
+ ```text
48
+ usage: main.py [-h] [-g GENERATIONS] [-s PROCESSER_CONFIG] [-o OUTPUTCONFIG] {local,hitokoto} ...
49
+ ```
50
+ | 参数 | 说明 | 默认值 |
51
+ | --- | --- | --- |
52
+ | `-g, --generations` | 生成次数 | `1` |
53
+ | `-s, --processor-config` | 处理器流水线配置 | `None` |
54
+ | `-o, --outputer-config` | 输出器流水线配置 | `None` |
55
+
56
+ 子命令:
57
+
58
+ - `local`:`-f/--file` 指定本地文件(默认 `./cowsay.txt`)。
59
+ - `hitokoto`:`-c/--category` 指定分类(可多选,默认全部)。
60
+
61
+ ## 配置
62
+
63
+ 处理器配置:
64
+ ```yaml
65
+ pipeline:
66
+ - processor: to_weak
67
+ - processor: i18n
68
+ params:
69
+ lan: [en, ja]
70
+
71
+ global: {}
72
+ ```
73
+ 输出器配置:
74
+ ```yaml
75
+ outputs:
76
+ - outputer: term
77
+ params:
78
+ auto_clear: True
79
+
80
+ global: {}
81
+ ```
82
+ `params` 会与 `global` 中同名步骤的配置合并,步骤级优先。
83
+
84
+ ## 扩展
85
+
86
+ 新增处理器:
87
+ ```python
88
+ from src.pipesay.processor.base import processor
89
+
90
+ @processor('shout')
91
+ def shout(sentences: list, mark: str = '!', log=None):
92
+ """给每句话加上强调符号"""
93
+ return [s + mark for s in sentences]
94
+ ```
95
+ 新增输出器:
96
+ ```python
97
+ from src.pipesay.outputers.base import outputer
98
+
99
+ @outputer('count')
100
+ def count(sentences: list, log=None):
101
+ """只统计句子数量"""
102
+ log(f"共 {len(sentences)} 句")
103
+ ```
104
+ 注册后即可在 YAML 中直接引用。函数签名中的 `sentences` 与 `log` 会被自动跳过,其余参数反射为可配置项。
105
+
106
+ 查看已注册的步骤:
107
+ ```python
108
+ from src.pipesay.processor.base import Processor
109
+ from src.pipesay.outputers.base import Outputer
110
+
111
+ Processor.list_all()
112
+ Outputer.list_all()
113
+ ```
114
+ ## 项目结构
115
+ ```text
116
+ src/pipesay/
117
+ ├── main.py # 入口
118
+ ├── constants/ # 常量
119
+ ├── fetcher/ # 获取器
120
+ ├── processor/ # 处理器与注册表
121
+ ├── outputers/ # 输出器与注册表
122
+ ├── core/ # 流水线执行器与注册装饰器
123
+ ├── parser/ # 命令行解析
124
+ └── utils/ # 通用工具
125
+ ```
126
+ ## 工作原理
127
+ ```text
128
+ Fetcher ──▶ Processor ──▶ Outputer
129
+ ▲ ▲
130
+ pipeline.yaml outputs.yaml
131
+ ```
132
+ 1. `Fetcher` 按模式返回 `list[str]`。
133
+ 2. `Processor` 读取 `pipeline` 段,按序加工并逐级传递。
134
+ 3. `Outputer` 读取 `outputs` 段,将结果交给输出器展示。
135
+
136
+ ## License
137
+
138
+ MIT
@@ -0,0 +1,122 @@
1
+ # PipeSay
2
+
3
+ > 一个可插拔的「句子流水线」框架:抓取 → 加工 → 输出。
4
+
5
+ PipeSay 将句子处理抽象为一条可配置的流水线。每个环节都通过装饰器注册、通过 YAML 组合,你无需改动核心代码,就能拼装出自己的玩法。
6
+
7
+ ## 设计理念
8
+
9
+ - **三段式流水线**:`Fetcher` → `Processor` → `Outputer`,职责清晰,互不耦合。
10
+ - **注册式插件系统**:`@processor('name')` / `@outputer('name')` 一键注册,签名自动反射为可配置参数。
11
+ - **YAML 驱动**:处理器与输出器均以声明式配置组合,支持全局参数与步骤级参数合并。
12
+ - **面向扩展**:核心只提供约定与调度,具体能力全部来自插件。
13
+
14
+ ## 安装
15
+ ```bash
16
+ pip install -r requirements.txt
17
+ ```
18
+
19
+ ## 快速开始
20
+
21
+ 从网络获取一言并输出到终端:
22
+ ```bash
23
+ python -m src.pipesay.main hitokoto -o test2.yaml
24
+ ```
25
+
26
+ 从本地文件读取、加工后输出:
27
+ ```bash
28
+ python -m src.pipesay.main local -f ./cowsay.txt -s test.yaml -o test2.yaml
29
+ ```
30
+ ## 命令行
31
+ ```text
32
+ usage: main.py [-h] [-g GENERATIONS] [-s PROCESSER_CONFIG] [-o OUTPUTCONFIG] {local,hitokoto} ...
33
+ ```
34
+ | 参数 | 说明 | 默认值 |
35
+ | --- | --- | --- |
36
+ | `-g, --generations` | 生成次数 | `1` |
37
+ | `-s, --processor-config` | 处理器流水线配置 | `None` |
38
+ | `-o, --outputer-config` | 输出器流水线配置 | `None` |
39
+
40
+ 子命令:
41
+
42
+ - `local`:`-f/--file` 指定本地文件(默认 `./cowsay.txt`)。
43
+ - `hitokoto`:`-c/--category` 指定分类(可多选,默认全部)。
44
+
45
+ ## 配置
46
+
47
+ 处理器配置:
48
+ ```yaml
49
+ pipeline:
50
+ - processor: to_weak
51
+ - processor: i18n
52
+ params:
53
+ lan: [en, ja]
54
+
55
+ global: {}
56
+ ```
57
+ 输出器配置:
58
+ ```yaml
59
+ outputs:
60
+ - outputer: term
61
+ params:
62
+ auto_clear: True
63
+
64
+ global: {}
65
+ ```
66
+ `params` 会与 `global` 中同名步骤的配置合并,步骤级优先。
67
+
68
+ ## 扩展
69
+
70
+ 新增处理器:
71
+ ```python
72
+ from src.pipesay.processor.base import processor
73
+
74
+ @processor('shout')
75
+ def shout(sentences: list, mark: str = '!', log=None):
76
+ """给每句话加上强调符号"""
77
+ return [s + mark for s in sentences]
78
+ ```
79
+ 新增输出器:
80
+ ```python
81
+ from src.pipesay.outputers.base import outputer
82
+
83
+ @outputer('count')
84
+ def count(sentences: list, log=None):
85
+ """只统计句子数量"""
86
+ log(f"共 {len(sentences)} 句")
87
+ ```
88
+ 注册后即可在 YAML 中直接引用。函数签名中的 `sentences` 与 `log` 会被自动跳过,其余参数反射为可配置项。
89
+
90
+ 查看已注册的步骤:
91
+ ```python
92
+ from src.pipesay.processor.base import Processor
93
+ from src.pipesay.outputers.base import Outputer
94
+
95
+ Processor.list_all()
96
+ Outputer.list_all()
97
+ ```
98
+ ## 项目结构
99
+ ```text
100
+ src/pipesay/
101
+ ├── main.py # 入口
102
+ ├── constants/ # 常量
103
+ ├── fetcher/ # 获取器
104
+ ├── processor/ # 处理器与注册表
105
+ ├── outputers/ # 输出器与注册表
106
+ ├── core/ # 流水线执行器与注册装饰器
107
+ ├── parser/ # 命令行解析
108
+ └── utils/ # 通用工具
109
+ ```
110
+ ## 工作原理
111
+ ```text
112
+ Fetcher ──▶ Processor ──▶ Outputer
113
+ ▲ ▲
114
+ pipeline.yaml outputs.yaml
115
+ ```
116
+ 1. `Fetcher` 按模式返回 `list[str]`。
117
+ 2. `Processor` 读取 `pipeline` 段,按序加工并逐级传递。
118
+ 3. `Outputer` 读取 `outputs` 段,将结果交给输出器展示。
119
+
120
+ ## License
121
+
122
+ MIT
pipesay-0.0.1/main.py ADDED
@@ -0,0 +1,22 @@
1
+ from src.pipesay.fetcher.fetcher import Fetcher
2
+ from src.pipesay.outputers import outputer_utils # noqa: F401
3
+ from src.pipesay.outputers.base import Outputer
4
+ from src.pipesay.parser.parser import arg_process
5
+ from src.pipesay.processor import processer_utils # noqa: F401
6
+ from src.pipesay.processor.base import Processor
7
+ from src.pipesay.utils.utils import clear_screen, enter_to_next
8
+
9
+
10
+ def main():
11
+ args = arg_process()
12
+ fetcher = Fetcher(args.get('fetcher_mode'), args.get('generations'), args.get('category'), args.get('file_path'),).new_fetcher()
13
+ fetch_sentences = fetcher()
14
+ processor = Processor(config_path=args.get('processer_config'))
15
+ speak_sentences = processor.process(fetch_sentences)
16
+ enter_to_next("已经处理完成,按回车开始输出")
17
+ clear_screen()
18
+ outputer = Outputer(config_path=args.get('outputer_config'))
19
+ outputer.fire(speak_sentences)
20
+
21
+ if __name__ == "__main__":
22
+ main()
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pipesay"
7
+ version = "0.0.1"
8
+ description = "一个用管道处理句子的趣味输出工具"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ "python-cowsay",
13
+ "desktop_notifier",
14
+ "jieba",
15
+ "openai",
16
+ "pyaml",
17
+ "requests",
18
+ "translators",
19
+ "urllib3"
20
+ ]
21
+
22
+ [project.scripts]
23
+ pipesay = "pipesay.main:main"
@@ -0,0 +1,8 @@
1
+ python-cowsay==6.1
2
+ desktop_notifier==6.2.0
3
+ jieba==0.42.1
4
+ openai==3.13.0
5
+ PyYAML==6.0.3
6
+ Requests==2.34.2
7
+ translators==6.0.4
8
+ urllib3==2.7.0
@@ -0,0 +1,9 @@
1
+ # src/pipesay/__init__.py
2
+ from pipesay.core.pipeline import Pipeline
3
+ from pipesay.core.registry import register
4
+ from pipesay.fetcher.fetcher import Fetcher
5
+ from pipesay.outputers.base import Outputer
6
+ from pipesay.processor.base import Processor
7
+
8
+ __version__ = "0.0.1"
9
+ __all__ = ["Fetcher", "Outputer", "Pipeline", "Processor", "register"]
File without changes
@@ -0,0 +1,3 @@
1
+ DEFAULT_FILE = "./cowsay.txt"
2
+ DEFAULT_LINES = ['我正在说一句废话\n', '你说得对\n', '敏捷的那啥越过啥玩意我忘了\n']
3
+ YIYAN_CATEGORY = {"a": "动画", "b": "漫画", "c": "游戏", "d": "文学", "e": "原创", "f": "来自网络", "g": "其他", "h": "影视", "i": "诗词", "j": "网易云", "k": "哲学", "l": "抖机灵"}
@@ -0,0 +1,89 @@
1
+ import asyncio
2
+ import inspect
3
+
4
+ import yaml
5
+
6
+
7
+ class Pipeline:
8
+ """
9
+ 通用管道执行器。
10
+ 子类只需要提供:
11
+ - registry:注册表字典
12
+ - section:配置里的键名('pipeline' / 'outputs')
13
+ - label:日志里显示的名字('Processor' / 'Outputer')
14
+ """
15
+ registry: dict = {}
16
+ section: str = ''
17
+ label: str = 'Pipeline'
18
+
19
+ def __init__(self, config_path=None, config_dict=None):
20
+ if config_dict:
21
+ self.config = config_dict
22
+ elif config_path:
23
+ with open(config_path, 'r', encoding='utf-8') as f:
24
+ self.config = yaml.safe_load(f)
25
+ else:
26
+ raise ValueError("必须提供 config_path 或 config_dict")
27
+
28
+ self.steps = self.config.get(self.section, [])
29
+ self.global_config = self.config.get('global', {})
30
+
31
+ def run(self, items: list) -> list:
32
+ current = items
33
+ total = len(self.steps)
34
+ for idx, step in enumerate(self.steps, 1):
35
+ name = step.get('name') or step.get('processor') or step.get('outputer')
36
+ params = step.get('params', {})
37
+
38
+ if name not in self.registry:
39
+ raise ValueError(
40
+ f"未知步骤: {name},已注册: {list(self.registry.keys())}"
41
+ )
42
+
43
+ self.log(f'⏳ [{idx}/{total}] 执行 {name}')
44
+ func = self.registry[name]['func']
45
+ final_params = {**self.global_config.get(name, {}), **params}
46
+
47
+ try:
48
+ result = self._call(func, current, final_params)
49
+ current = result
50
+ self.log(f'✅ {name} 完成')
51
+ except Exception as e:
52
+ self.log(f'❌ {name} 失败: {e}')
53
+ raise
54
+
55
+ self.log(f'{self.label} 全部处理完成!')
56
+ return current
57
+
58
+ def _call(self, func, items, params):
59
+ """统一注入 log,统一处理 async"""
60
+ params = {**params, 'log': self.log}
61
+ result = func(items, **params)
62
+ if inspect.iscoroutine(result):
63
+ result = asyncio.run(result)
64
+ return result
65
+
66
+ def log(self, message: str):
67
+ print(message)
68
+
69
+ @classmethod
70
+ def list_all(cls):
71
+ print("\n" + "=" * 60)
72
+ print(f"已注册的 {cls.label}:")
73
+ print("=" * 60)
74
+ for name, info in cls.registry.items():
75
+ print(f"\n【{name}】")
76
+ if info['doc']:
77
+ print(f" 说明: {info['doc']}")
78
+ if info['params']:
79
+ print(" 参数:")
80
+ for pname, pinfo in info['params'].items():
81
+ required = "必填" if pinfo['required'] else "可选"
82
+ default = (
83
+ f",默认: {pinfo['default']}"
84
+ if pinfo['default'] is not None else ""
85
+ )
86
+ print(f" - {pname}: {pinfo['type']} ({required}{default})")
87
+ else:
88
+ print(" 无参数")
89
+ print("=" * 60)
@@ -0,0 +1,38 @@
1
+ import inspect
2
+ from collections.abc import Callable
3
+
4
+
5
+ def register(registry: dict, name: str | None = None):
6
+ """
7
+ 通用注册装饰器。
8
+ 用法:
9
+ @register(PROCESSOR_REGISTRY, 'reverse')
10
+ def reverse(sentences, log=None): ...
11
+ """
12
+ def decorator(func: Callable):
13
+ key = name or func.__name__
14
+ registry[key] = {
15
+ 'func': func,
16
+ 'params': extract_params(func),
17
+ 'doc': func.__doc__ or '',
18
+ }
19
+ return func # 不套 wrapper,注册就是注册
20
+ return decorator
21
+
22
+
23
+ def extract_params(func: Callable) -> dict:
24
+ """从函数签名反射出可配置参数,跳过 self / sentences / log"""
25
+ sig = inspect.signature(func)
26
+ params = {}
27
+ for pname, param in sig.parameters.items():
28
+ if pname in ('self', 'sentences', 'log'):
29
+ continue
30
+ if param.default is inspect.Parameter.empty:
31
+ params[pname] = {'type': 'any', 'default': None, 'required': True}
32
+ else:
33
+ params[pname] = {
34
+ 'type': type(param.default).__name__,
35
+ 'default': param.default,
36
+ 'required': False,
37
+ }
38
+ return params
@@ -0,0 +1,142 @@
1
+ import random
2
+ import time
3
+
4
+ import requests
5
+ from requests.adapters import HTTPAdapter
6
+ from urllib3 import Retry
7
+
8
+ from src.pipesay.constants.constants import YIYAN_CATEGORY
9
+
10
+
11
+ class Fetcher:
12
+ def __init__(self, fetch_mode: str, generations: int, category: list, file_path: str):
13
+ self.mode = fetch_mode
14
+ self.generations = generations
15
+ self.category = category
16
+ self.file_path = file_path
17
+
18
+ def new_fetcher(self):
19
+ fdict = {
20
+ "hitokoto": self.hitokoto,
21
+ "local": self.local
22
+ }
23
+ func = fdict.get(self.mode)
24
+ if func is None:
25
+ raise ValueError(f"{self.mode}不是一个有效的模式")
26
+ return func
27
+
28
+
29
+ def hitokoto(self):
30
+ """
31
+ 控制对Hitokoto服务的请求及对其结果的格式化
32
+
33
+ Args:
34
+ generations(int): 需要的句子数量
35
+ category(str): 需要的分类
36
+
37
+
38
+ Returns:
39
+ list: 一个包含字符串的列表,每个字符串是一个完整的句子。
40
+ 句子格式为:"“句子内容”\n ————出处"
41
+ 例如: [
42
+ "“你不努力,你得到的一切一定会被夺走!”\n ————Posherlunch"
43
+ "“生命璀璨美丽,却成为众人的囚笼。”\n ————黑暗之魂2"
44
+ ]
45
+ """
46
+
47
+ sentences = []
48
+ last_sentence = ""
49
+ retry_word = ""
50
+
51
+ while len(sentences) < self.generations:
52
+ print(f"{retry_word}正在向Hitokoto一言获取{len(sentences) + 1}/{self.generations}个句子...", end=" ")
53
+ fetch_sentence, author, from_work = self._fetch_hitokoto()
54
+ print(" ✅")
55
+
56
+ if len(sentences) == 0 or fetch_sentence != last_sentence:
57
+ if author is not None:
58
+ sen_from = f"————{author} 《{from_work}》"
59
+ else:
60
+ sen_from = f"————《{from_work}》"
61
+ width = len(fetch_sentence) + 4
62
+ sentences.append(f"“{fetch_sentence}”\n{sen_from:>{width}}")
63
+ last_sentence = fetch_sentence
64
+ retry_word = ""
65
+ elif last_sentence == fetch_sentence:
66
+ print("由于api方缓存问题,此句和上句重复,本程序将睡2秒再重新获取😋")
67
+ retry_word = "🔄 重试:"
68
+ time.sleep(2)
69
+
70
+ return sentences
71
+
72
+
73
+ def _category_to_dict(self):
74
+ """将分类名称列表转换为 Hitokoto API 的分类代码。
75
+
76
+
77
+ Returns:
78
+ dict: API 参数字典,格式为 {'c': ['a', 'k']}
79
+ 如果 categories 为空,返回空字典 {}
80
+ """
81
+
82
+ choose = []
83
+ for c in self.category:
84
+ for category, name in YIYAN_CATEGORY.items():
85
+ if c == name:
86
+ choose.append(category)
87
+ break
88
+ return {"c": choose} if choose else {}
89
+
90
+
91
+ def _fetch_hitokoto(self):
92
+ """
93
+ 向Hitokoto服务请求并获得句子信息
94
+
95
+ Args:
96
+ params(dict): 需要在请求后附加的url参数,以此控制筛选分类
97
+ Returns:
98
+ str: 句子本身
99
+ str: 句子的言者/作者
100
+ str: 出处
101
+
102
+
103
+ Raises:
104
+ requests.exceptions.Timeout: 请求超时被抛出
105
+ """
106
+
107
+ retry_strategy = Retry(
108
+ total=3, # 最多重试 3 次(不算第一次请求)
109
+ backoff_factor=1, # 重试间隔:1s, 2s, 4s(指数退避)
110
+ status_forcelist=[500, 502, 503, 504], # 碰到这些 HTTP 状态码就重试
111
+ allowed_methods=["GET", "POST"] # 允许重试的方法(默认只有 GET)
112
+ )
113
+
114
+ session = requests.Session()
115
+ adapter = HTTPAdapter(max_retries=retry_strategy)
116
+ session.mount('http://', adapter)
117
+ session.mount('https://', adapter)
118
+
119
+ try:
120
+ r = session.get('https://v1.hitokoto.cn', timeout=(3.5, 6.5), params=self._category_to_dict())
121
+ except requests.exceptions.Timeout:
122
+ print("请求超时,请您检查网络和Hitokoto服务状态,并稍后再试")
123
+ raise
124
+
125
+ return r.json()["hitokoto"], r.json()["from_who"], r.json()["from"]
126
+
127
+
128
+
129
+
130
+ def local(self):
131
+ """
132
+ 从本地文件中挑选句子
133
+
134
+ Returns:
135
+ list: 句子列表
136
+ """
137
+ sentences = []
138
+ with open(file=self.file_path, mode="r") as f:
139
+ local_sentence = f.readlines()
140
+ for i in range(self.generations):
141
+ sentences.append(random.choice(local_sentence))
142
+ return sentences
@@ -0,0 +1,21 @@
1
+ from src.pipesay.core.pipeline import Pipeline
2
+ from src.pipesay.core.registry import register
3
+
4
+ OUTPUTER_REGISTRY: dict = {}
5
+
6
+
7
+ def outputer(name: str | None = None):
8
+ return register(OUTPUTER_REGISTRY, name)
9
+
10
+
11
+ class Outputer(Pipeline):
12
+ registry = OUTPUTER_REGISTRY
13
+ section = 'outputs'
14
+ label = 'Outputer'
15
+
16
+ def fire(self, sentences: list) -> None:
17
+ self.run(sentences)
18
+
19
+ @staticmethod
20
+ def list_outputers():
21
+ Outputer.list_all()
@@ -0,0 +1,49 @@
1
+ import asyncio
2
+
3
+ from desktop_notifier import DesktopNotifier, Urgency
4
+
5
+ from src.pipesay.outputers.base import outputer
6
+ from src.pipesay.utils.utils import clear_screen, enter_to_next
7
+
8
+
9
+ @outputer('term')
10
+ def normal_terminal(sentences: list, auto_clear: bool=True, log=None):
11
+ for index, text in enumerate(sentences):
12
+ print(text)
13
+ if index + 1 != len(sentences):
14
+ print("=" * 30)
15
+ enter_to_next("按回车来看下一句吧!")
16
+ print("\n\n")
17
+ if auto_clear:
18
+ clear_screen()
19
+
20
+
21
+ @outputer('file')
22
+ def to_file(sentences: list, file_path: str, log=None):
23
+ lines = [item + '\n\n' for item in sentences]
24
+ with open(file=file_path, mode='a', encoding='utf-8') as f:
25
+ f.writelines(lines)
26
+
27
+ @outputer('notify')
28
+ async def notify(sentences: list, level: str='normal', log=None):
29
+ URGENCY_MAP = {
30
+ "low": Urgency.Low,
31
+ "normal": Urgency.Normal,
32
+ "critical": Urgency.Critical,
33
+ }
34
+
35
+ urgency = URGENCY_MAP[level]
36
+
37
+ notifier = DesktopNotifier(app_name="批量通知演示")
38
+
39
+ for message in sentences:
40
+ await notifier.send(
41
+ title="PipeSay",
42
+ message=message,
43
+ urgency=urgency,
44
+ sound=True,
45
+ )
46
+
47
+ await asyncio.sleep(3)
48
+
49
+ log(f"已发送 {len(sentences)} 条通知,类型:{level}")
@@ -0,0 +1,53 @@
1
+ import argparse
2
+
3
+ from src.pipesay.constants import constants
4
+
5
+
6
+ def _arg_parser() -> argparse.Namespace:
7
+ parser = argparse.ArgumentParser(description='PipeSay句子流水线')
8
+ parser.add_argument('-g', '--generations', type=int, default=1, help='生成次数(默认:1)')
9
+ parser.add_argument('-s', '--processor-config', dest="processer_config", type=str, default=None, help='processer流水线配置')
10
+ parser.add_argument('-o', '--outputer-config', dest="outputer_config", type=str, default=None, help='outputer流水线配置')
11
+
12
+ subparsers = parser.add_subparsers(dest='mode', required=True, help='选择获取模式')
13
+
14
+ local_parser = subparsers.add_parser('local', help='从本地文件读取一言')
15
+ local_parser.add_argument('-f', '--file', default=constants.DEFAULT_FILE, help='本地文件')
16
+
17
+ hitokoto_parser = subparsers.add_parser('hitokoto', help='从网络获取')
18
+ hitokoto_parser.add_argument('-c', '--category', nargs='*', choices=constants.YIYAN_CATEGORY.values(), default=constants.YIYAN_CATEGORY.values(), help='分类')
19
+
20
+ return parser.parse_args()
21
+
22
+
23
+ def arg_process():
24
+ args = _arg_parser()
25
+ if args.generations <= 0:
26
+ raise ValueError("请输入一个正整数生成次数")
27
+ if args.mode == "local":
28
+ if args.file == constants.DEFAULT_FILE:
29
+ with open(args.file, 'a+') as f:
30
+ first_char = f.read(1)
31
+ if not first_char:
32
+ print("语录文件是空的,已为您创建并写入了一定的内置语句")
33
+ with open(args.file, 'w') as fw:
34
+ fw.writelines(constants.DEFAULT_LINES)
35
+ else:
36
+ try:
37
+ with open(args.file, 'r') as f:
38
+ first_char = f.read(1)
39
+ if not first_char:
40
+ raise RuntimeError(f"{args.file}是空的!")
41
+ except FileNotFoundError:
42
+ print("没有找到您的文件")
43
+ raise
44
+
45
+ config = {
46
+ "fetcher_mode": args.mode,
47
+ "generations": args.generations,
48
+ "file_path": args.file if args.mode == 'local' else None,
49
+ "category": args.category if args.mode == 'hitokoto' else None,
50
+ "processer_config": args.processer_config,
51
+ "outputer_config": args.outputer_config
52
+ }
53
+ return config
@@ -0,0 +1,21 @@
1
+ from src.pipesay.core.pipeline import Pipeline
2
+ from src.pipesay.core.registry import register
3
+
4
+ PROCESSOR_REGISTRY: dict = {}
5
+
6
+
7
+ def processor(name: str | None = None):
8
+ return register(PROCESSOR_REGISTRY, name)
9
+
10
+
11
+ class Processor(Pipeline):
12
+ registry = PROCESSOR_REGISTRY
13
+ section = 'pipeline'
14
+ label = 'Processor'
15
+
16
+ def process(self, sentences: list) -> list:
17
+ return self.run(sentences)
18
+
19
+ @staticmethod
20
+ def list_processers():
21
+ Processor.list_all()
@@ -0,0 +1,204 @@
1
+ import json
2
+ import random
3
+ import time
4
+ import unicodedata
5
+ from concurrent.futures import ThreadPoolExecutor
6
+
7
+ import jieba
8
+ import translators as ts
9
+ from cowsay import cowsay, list_cows
10
+ from openai import OpenAI
11
+
12
+ from src.pipesay.processor.base import processor
13
+
14
+
15
+ @processor('reverse')
16
+ def reverse(sentences: list, log=None):
17
+ """反转字符串"""
18
+ rl = []
19
+ for s in sentences:
20
+ rl.append(s[::-1])
21
+ return rl
22
+
23
+ @processor('to_weak')
24
+ def toweak(sentences: list, log=None):
25
+ """让你的话变得虚弱无比"""
26
+ PUNCT = set(",。!?;:、")
27
+ return_list = []
28
+
29
+ for s in sentences:
30
+ parts = []
31
+ current = ""
32
+ for char in s:
33
+ if char in PUNCT:
34
+ if current:
35
+ parts.append(current)
36
+ parts.append(char)
37
+ current = ""
38
+ else:
39
+ current += char
40
+ if current:
41
+ parts.append(current)
42
+
43
+ # 先找出所有可分词的片段索引
44
+ word_part_indices = [i for i, p in enumerate(parts) if p not in PUNCT]
45
+
46
+ # 整句只选一个片段来插入省略号
47
+ if word_part_indices:
48
+ chosen = random.choice(word_part_indices)
49
+ else:
50
+ chosen = None
51
+
52
+ result = []
53
+ for i, part in enumerate(parts):
54
+ if part in PUNCT:
55
+ result.append(part)
56
+ else:
57
+ words = jieba.lcut(part)
58
+ if not words:
59
+ continue
60
+ if i == chosen:
61
+ idx = random.randint(1, len(words) - 1) if len(words) > 1 else 0
62
+ words.insert(idx, "……")
63
+ result.extend(words)
64
+
65
+ return_list.append(''.join(result))
66
+
67
+ return return_list
68
+
69
+ @processor('i18n')
70
+ def i18n(sentences, lan: list, log=None):
71
+ """保持你原来的函数签名和封装结构"""
72
+ def _translate_one(s, l):
73
+ translated = ts.translate_text(s, translator='bing', to_language=l)
74
+ fixed = unicodedata.normalize('NFKC', translated)
75
+ time.sleep(0.5)
76
+ log(f"{l}获取完成啦!为防止风控,i18n工具需要0.5秒再打请求")
77
+ return fixed
78
+
79
+ rl = []
80
+ with ThreadPoolExecutor(max_workers=2) as executor:
81
+ for s in sentences:
82
+ i18n_list = []
83
+ futures = [executor.submit(_translate_one, s, l) for l in lan]
84
+ for f in futures:
85
+ i18n_list.append(f.result())
86
+ result = ''.join(['\n\n' + item for item in i18n_list])
87
+ rl.append(result)
88
+ return rl
89
+
90
+
91
+ @processor('ads')
92
+ def ads(sentences, base_url: str, model: str, api_key: str, category: list | None = None, log=None):
93
+
94
+ if category is None:
95
+ category = ['美食', '科技', '美妆', '旅行', '亲子']
96
+
97
+ client = OpenAI(
98
+ api_key=api_key,
99
+ base_url=base_url,
100
+ )
101
+
102
+ system_prompt = """
103
+ You are an AI assistant skilled at generating short ad copy in the style of Google Ads.
104
+ The user will provide a list of categories (e.g., ['Food', 'Tech', 'Beauty']), and you need to randomly select one category from the list and generate an ad for it.
105
+
106
+ Requirements:
107
+
108
+ Style: Google Ads RSA style. The title should include a selling point + a call to action (CTA). The description should add trust signals or promotional offers.
109
+
110
+ In a user's request, a number is usually attached as the number of ads to generate. You need to generate according to that number, and always return using a JSON list even though the user may only ask for one.
111
+
112
+ Output: Strict JSON format with the following two fields:
113
+
114
+ "title": no more than 30 characters, attention-grabbing
115
+
116
+ "description": no more than 45 characters, including a call to action
117
+
118
+ Product/brand names must be fictional but sound realistic and trustworthy.
119
+
120
+ Language adaptation: Generate the ad in the primary language of the input category list (if mixed, prefer Chinese).
121
+
122
+ Output only the JSON object – no extra text, comments, or Markdown formatting.
123
+
124
+ Example 1 (Chinese):
125
+ INPUT: ['美食', '科技', '美妆', '旅行', '亲子'],2
126
+ OUTPUT: [{"title": "5999元起|东京七日半自助游", "description": "正规旅行社,资深导游带队,无强制购物。立即询价!"}, {"title":"青春期孩子不听管教 | 育儿帮", "description": "点击连线专业咨询师"}]
127
+
128
+ Example 2 (English):
129
+ INPUT: ['Electronics', 'Fitness', 'Home'],1
130
+ OUTPUT: [{"title": "50% Off|SonicGlide Toothbrush", "description": "Advanced sonic cleaning. Free shipping, order now!"}]
131
+ """
132
+
133
+ user_prompt = f"{category},{len(sentences)}"
134
+
135
+ messages = [{"role": "system", "content": system_prompt},
136
+ {"role": "user", "content": user_prompt}]
137
+
138
+ response = client.chat.completions.create(
139
+ model=model,
140
+ messages=messages,
141
+ response_format={
142
+ 'type': 'json_object'
143
+ }
144
+ )
145
+
146
+ llm_result: list = json.loads(response.choices[0].message.content)
147
+
148
+ ad_text: list = [f"\n\n[Ads]\n{r['title']}\n{r['description']}" for r in llm_result]
149
+ result = [x + y for x, y in zip(sentences, ad_text)]
150
+
151
+ return result
152
+
153
+ @processor('cowsay')
154
+ def cow(sentences: list, cow: str = "default", random_cow: bool = False, log=None):
155
+ rl = []
156
+
157
+ if random_cow:
158
+ cow = random.choice(list_cows())
159
+
160
+ for sentence in sentences:
161
+ lines = sentence.split("\n")
162
+ max_line_len = max(len(line) for line in lines)
163
+ dynamic_width = max_line_len + 4
164
+ rl.append(cowsay(sentence, cow=cow, width=dynamic_width))
165
+
166
+ return rl
167
+
168
+
169
+ @processor('ban')
170
+ def ban(sentences: list, ban_count: int=1, log=None):
171
+
172
+ rl = []
173
+
174
+ for s in sentences:
175
+ cur_sen = s
176
+ for a in range(ban_count):
177
+ index = random.randint(0, len(cur_sen) - 1)
178
+ cur_sen = cur_sen[:index] + "**" + cur_sen[index+1:]
179
+ rl.append(cur_sen)
180
+
181
+ return rl
182
+
183
+ @processor('rep2')
184
+ def replace_repeat(sentences: list, rep_sen: str, log=None):
185
+ """
186
+ 将rep_sen中的内容循环重复替换sen内容
187
+
188
+ Args:
189
+ rep_sen(str): 准备好的来循环替换的句子
190
+
191
+ Example:
192
+ sentence为20个字符
193
+ >>> print(replace_repeat("Goodbye"))
194
+ GoodbyeGoodbyeGoodby
195
+ 注意,有一个e因为超出句子长度被截断了哦
196
+ """
197
+ rl = []
198
+
199
+ for s in sentences:
200
+ target_len = len(s)
201
+ q, r = divmod(target_len, len(rep_sen))
202
+ result = rep_sen * q + rep_sen[:r]
203
+ rl.append(result)
204
+ return rl
@@ -0,0 +1,20 @@
1
+ import os
2
+ import subprocess
3
+
4
+
5
+ def enter_to_next(prompt):
6
+ """
7
+ 对程序进行阻塞使用户可以阅读句子/提示信息等再进行下一步
8
+
9
+ Args:
10
+ prompt(str): 向用户展示的提示信息
11
+ """
12
+
13
+ input(prompt)
14
+
15
+ def clear_screen():
16
+ """清理屏幕上原有信息"""
17
+ if os.name == 'nt':
18
+ subprocess.run('cls', shell=True, check=False)
19
+ else:
20
+ subprocess.run('clear', shell=True, check=False)