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.
- pipesay-0.0.1/.github/workflows/publish.yml +70 -0
- pipesay-0.0.1/.gitignore +16 -0
- pipesay-0.0.1/LICENSE +7 -0
- pipesay-0.0.1/PKG-INFO +138 -0
- pipesay-0.0.1/README.md +122 -0
- pipesay-0.0.1/main.py +22 -0
- pipesay-0.0.1/pyproject.toml +23 -0
- pipesay-0.0.1/requirements.txt +8 -0
- pipesay-0.0.1/src/pipesay/__init__.py +9 -0
- pipesay-0.0.1/src/pipesay/constants/__init__.py +0 -0
- pipesay-0.0.1/src/pipesay/constants/constants.py +3 -0
- pipesay-0.0.1/src/pipesay/core/pipeline.py +89 -0
- pipesay-0.0.1/src/pipesay/core/registry.py +38 -0
- pipesay-0.0.1/src/pipesay/fetcher/fetcher.py +142 -0
- pipesay-0.0.1/src/pipesay/outputers/base.py +21 -0
- pipesay-0.0.1/src/pipesay/outputers/outputer_utils.py +49 -0
- pipesay-0.0.1/src/pipesay/parser/parser.py +53 -0
- pipesay-0.0.1/src/pipesay/processor/base.py +21 -0
- pipesay-0.0.1/src/pipesay/processor/processer_utils.py +204 -0
- pipesay-0.0.1/src/pipesay/utils/utils.py +20 -0
|
@@ -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/
|
pipesay-0.0.1/.gitignore
ADDED
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
|
pipesay-0.0.1/README.md
ADDED
|
@@ -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,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,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)
|