This commit is contained in:
2026-01-10 00:59:25 +08:00
parent 9da3d14752
commit 92ef98ce9b
27 changed files with 31787 additions and 175 deletions

View File

@@ -1,85 +0,0 @@
LOG_LEVEL=DEBUG
IS_DEV=true
POSTGRES_HOST=1.15.101.247
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=abearxiong!
POSTGRES_DB=postgres
# DATABASE_URL=postgresql://postgres:abearxiong!@1.15.101.247:5432/postgres
DATABASE_URL=postgresql://postgres:abearxiong@118.196.32.29:5432/postgres
REDIS_HOST=light.xiongxiao.me
REDIS_PORT=6379
REDIS_PASSWORD=abearxiong!
REDIS_DB=0
# POCKETBASE
POCKETBASE_URL=https://pocketbase.pro.xiongxiao.me
POCKETBASE_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTc3NTYyODY5NywiaWQiOiI0cGRtMXF4cjlkOXNuam0iLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.2ABYhI0ayxpEV09gNvWUIM0lXfAx7hfBT02WcVPmyNw
# S3
S3_ACCESS_KEY_ID=AKLTOWNhNmJkNDJmNzFkNGI3MDlmMWQzYTA2ZjBkYTc2YTg
S3_ACCESS_KEY_SECRET=TWpjME9EVm1OVFJtTkROaE5ESXlaR0ptWlRnd1lqVm1Nems0TW1Ka1pUZw==
S3_REGION=cn-shanghai
S3_BUCKET_NAME=envision
S3_ENDPOINT=https://tos-s3-cn-shanghai.volces.com
# Minio 配置
MINIO_ENDPOINT=light.xiongxiao.me
MINIO_PORT=9000
MINIO_BUCKET_NAME=resources
MINIO_USE_SSL=false
MINIO_ACCESS_KEY=abearxiong
MINIO_SECRET_KEY=xiongxiao
# 域名
DOMAIN=xiongxiao.me
PORT=4005
# 代理配置
PROXY_DOMAIN=kevisual.xiongxiao.me
PROXY_RESOURCES=http://localhost:9000/resources
PROXY_ALLOWED_ORIGINS=localhost,xiongxiao.me
KEVISUAL_NEW_API_KEY=sk-YyVo5WqJBmAnhIPfww9XpUPvHNhsuiXs9a1OSfBul94d7O47
KEVISUAL_TOKEN="st_c7kyhg7sfhhhpiogydyogpoqzgzrnas7"
KEVISUAL_PASSWORD=123456xx
## gitea
GITEA_URL=https://git.xiongxiao.me
GITEA_TOKEN=18cd3c00308c3813765dde41d093d48bed76fabd
## ---- AI ----
# BAILIAN API
BAILIAN_API_KEY='sk-0fc39ea048484ccf9e35e4ed4b4950be'
ZHIPU_API_KEY="6e7a1bc2760a4bd79c6f436b552527be.2j8Ob751NKi6oiVY"
MINIMAX_API_KEY="sk-cp-_nvABjDELuG_o3_vmvlo0uAY1jHJAxglKqKly8ihAxKJcbCyvwqsld08c3R4QZbNfocMn1juB_FdUc1sdjC-gXj5unVykTJ2a6THYaWozQkNyJ5FwJ_aJdI"
# jimeng API
JIMENG_API_KEY=4e962fc85078d5bfc02c9882bfe659eb
JIMENG_API_URL=https://jimeng-api.kevisual.cn/v1
JIMENG_TIMEOUT=300000
VOLCENGINE_AUC_APPID=6968490116
VOLCENGINE_AUC_TOKEN=t1WIgIEUswuunOReyW8kiRCe5lW_lcFB
#-------
DATA_WEBSITE_ID=5fd42d1d-109e-43ab-b3a7-d4fda0c92d13
## 微信
# 微信开放平台 登陆
WX_OPEN_APP_ID=wx9378885c8390e09b
WX_OPEN_APP_SECRET=4a0d588fe0de9713ad0a7e680be3d225
# 微信公众号 登陆
WX_MP_APP_ID=wxff97d569b1db16b6
WX_MP_APP_SECRET=012d84d0d2b914de95f4e9ca84923aed
##
# Queue
QUEUE_CONCURRENCY=5
QUEUE_MAX_FAILED=10
FEISHU_NOTIFY_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/c1c32e36-ddc6-4965-8943-fc826f4f5060

View File

@@ -19,6 +19,4 @@ S3_ACCESS_KEY_SECRET=your_secret_key
S3_REGION=cn-beijing S3_REGION=cn-beijing
S3_ENDPOINT=tos-cn-beijing.volces.com S3_ENDPOINT=tos-cn-beijing.volces.com
# Queue FEISHU_NOTIFY_WEBHOOK_URL=your_feishu_webhook_url
QUEUE_CONCURRENCY=5
QUEUE_MAX_FAILED=10

2
prompts/.gitignore vendored
View File

@@ -2,5 +2,5 @@ node_modules
storage storage
.env # .env
!.env*example !.env*example

File diff suppressed because it is too large Load Diff

14010
prompts/data/sentence-01.json Normal file

File diff suppressed because it is too large Load Diff

396
prompts/plan/哲理.md Normal file
View File

@@ -0,0 +1,396 @@
# 1000条哲理话生成规划
## 一、项目概述
本规划旨在建立一个系统化的哲理话生成体系通过关键词层级结构、主题分类、句式模板的组合实现高效批量生成1000条高质量哲理话语。整体采用「主题+维度+意象+句式」的模块化设计,确保内容的深度与多样性。
---
## 二、关键词层级结构
### 2.1 第一层主主题关键词20个
| 序号 | 主题 | 核心维度 | 产出权重 |
|:---:|------|---------|:-------:|
| 1 | 人生 | 起落、意义、旅程 | 8% |
| 2 | 时间 | 流逝、珍惜、永恒 | 7% |
| 3 | 成长 | 蜕变、代价、成熟 | 7% |
| 4 | 孤独 | 独处、清醒、边缘 | 6% |
| 5 | 选择 | 路口、承担、割舍 | 6% |
| 6 | 自由 | 边界、主宰、释放 | 5% |
| 7 | 痛苦 | 磨砺、觉醒、转化 | 5% |
| 8 | 爱 | 付出、放手、成全 | 5% |
| 9 | 死亡 | 终结、新生、虚无 | 4% |
| 10 | 真相 | 本质、表象、谎言 | 4% |
| 11 | 欲望 | 克制、追求、放下 | 4% |
| 12 | 坚持 | 执着、韧劲、突破 | 4% |
| 13 | 放下 | 释然、接纳、告别 | 4% |
| 14 | 觉醒 | 开悟、看见、醒来 | 4% |
| 15 | 平凡 | 普通、真实、踏实 | 4% |
| 16 | 意义 | 价值、方向、目的 | 4% |
| 17 | 沉默 | 安静、力量、倾听 | 4% |
| 18 | 命运 | 注定、改变、巧合 | 4% |
| 19 | 人性 | 善恶、复杂、真实 | 4% |
| 20 | 世界 | 规则、真相、边界 | 3% |
### 2.2 第二层子主题关键词每个主主题5-10个
#### 人生主题扩展
```
起点、终点、过程、意义、价值、角色、舞台、角色、剧本、观众、演员
```
#### 时间主题扩展
```
过去、现在、未来、瞬间、永恒、季节、昼夜、钟表、沙漏、流水、光阴
```
#### 成长主题扩展
```
伤痕、蜕变、破茧、跌倒、镜子、旧我、觉醒、代价、忍耐、成熟、枯荣
```
#### 孤独主题扩展
```
独处、夜晚、人群疏离、无人理解、自我对话、清醒者、边缘人、静默、影子、星空
```
#### 选择主题扩展
```
路口、岔路、方向、代价、后悔、坚定、承担、割舍、决定、权衡、放手
```
#### 自由主题扩展
```
翅膀、天空、边界、束缚、解脱、随心所欲、主宰、释放、翱翔、远方
```
#### 痛苦主题扩展
```
伤疤、火焰、锤炼、熔炉、泪水、黑夜、黎明、破碎、重建、结晶
```
#### 爱的主题扩展
```
温暖、刺痛、放手、成全、燃烧、熄灭、靠近、远离、心跳、温柔
```
### 2.3 第三层:意象/隐喻关键词通用型50个
| 类别 | 意象词库 |
|-----|---------|
| 自然 | 光、暗、路、门、风、火、水、树、种子、根、山、海、雾、雨、雪、霜、雷、电、云、星、月、日 |
| 生物 | 花、草、叶、果实、飞鸟、游鱼、蝴蝶、蜜蜂、蚂蚁、狮子、狼、鹰、蛇 |
| 物品 | 钟、镜、书、信、灯、灯塔、钥匙、锁、刀、剑、帆、船、桥、窗、门、塔 |
| 抽象 | 影、回声、脚步、呼吸、脉搏、血液、泪水、汗水、尘埃、烟雾、碎片、拼图 |
| 空间 | 路口、渡口、山顶、谷底、十字路口、岔路口、隧道、迷宫、花园、荒原、森林 |
### 2.4 第四层:动词/状态词30个
| 类别 | 动词词库 |
|-----|---------|
| 动态 | 燃烧、沉淀、穿越、凝视、听见、醒来、破碎、重建、等待、放手、坠落、升起 |
| 状态 | 沉默、挣扎、回归、追随、停留、遗忘、铭记、看清、承认、接受、抵抗、屈服 |
| 变化 | 生长、凋零、绽放、枯萎、流动、凝固、溶解、凝结、扩散、收缩、上升、下降 |
| 感知 | 感受、体会、领悟、觉醒、明白、理解、懂得、发现、寻找、追求、失去、获得 |
### 2.5 第五层哲学句式模板20种核心句式
#### A类对比型强调对立统一
```
1. 真正的____不是____而是____。
2. 越____越____。
3. ____与____从来不是对立的。
4. 不是____太远而是____太近。
5. ____从不说谎____却常常欺骗。
```
#### B类因果型揭示内在逻辑
```
6. 当你____你才懂得____。
7. 只有____过____的人才明白____。
8. ____教会我____才是____。
9. 经历过____才知道____的可贵。
10. ____的代价是____。
```
#### C类隐喻型借助意象传达
```
11. ____像____终将____。
12. ____不会说话却教会了我____。
13. ____是____的镜子照见____。
14. 在____中我看见了____。
15. ____如同________才是____。
```
#### D类判断型直接陈述真理
```
16. 不怕____只怕____。
17. ____从不是____而是____。
18. 最高级的____是____。
19. 最深的____往往____。
20. ____的尽头是____。
```
---
## 三、主题分类体系
### 3.1 四大主题分区
```
┌─────────────────────────────────────────────────────┐
│ 哲理话主题分类 │
├─────────────┬─────────────┬─────────────┬───────────┤
│ 人生智慧 │ 情感关系 │ 成长蜕变 │ 世界观 │
├─────────────┼─────────────┼─────────────┼───────────┤
│ 人生意义 │ 爱与被爱 │ 成长代价 │ 时间永恒 │
│ 命运安排 │ 孤独本质 │ 痛苦转化 │ 真相假象 │
│ 选择承担 │ 关系边界 │ 平凡真实 │ 自由束缚 │
│ 放下执念 │ 付出回报 │ 自我认知 │ 人性善恶 │
│ 活在当下 │ 放手成全 │ 坚持放弃 │ 世界规则 │
└─────────────┴─────────────┴─────────────┴───────────┘
```
### 3.2 每主题产出配额
| 主题分区 | 主主题数 | 每主题产出 | 小计 |
|---------|:-------:|:---------:|-----:|
| 人生智慧 | 5 | 70 | 350 |
| 情感关系 | 4 | 70 | 280 |
| 成长蜕变 | 5 | 70 | 350 |
| 世界观 | 6 | 50 | 300 |
| 通用意象 | - | - | 100补足 |
| 特殊句式 | - | - | 120补足 |
| **总计** | **20** | - | **1000** |
---
## 四、批量生成策略
### 4.1 组合爆炸法
```
公式:主主题 × 子主题 × 意象 × 动词 × 句式模板 = 产出
示例计算:
20个主主题 × 8个子主题 × 50个意象 × 30个动词 × 20个句式 = 480,000种可能
实际筛选只需从中精选1000条高质量内容
```
### 4.2 三大生成技法
#### 技法一:模块填充法
```
步骤:
1. 选择主主题(如:孤独)
2. 选择子主题(如:独处)
3. 选择意象(如:灯塔)
4. 选择动词(如:守望)
5. 选择句式真正的____不是____而是____
产出:真正的孤独,不是独处,而是身处人群中却无人理解。
```
#### 技法二:意象移植法
```
步骤:
1. 固定一个意象(如:种子)
2. 套用多个主主题
产出:
- 关于成长:种子从不着急,它知道该发芽的时候自然会发芽。
- 关于坚持:种子用几年的时间,只为换来几天的花开。
- 关于孤独:种子独自在泥土里,完成自己的生长。
- 关于时间:种子记得每一个春天的约定,从不迟到。
```
#### 技法三:句式复用法
```
步骤:
1. 固定一个句式____不会说话却教会了我____
2. 更换主主题和意象
产出:
- 河流不会说话,却教会了我如何面对曲折。
- 镜子不会说话,却教会了我认识真实的自己。
- 黑夜不会说话,却教会了我珍惜光明。
- 书本不会说话,却教会了我理解复杂的人生。
```
---
## 五、执行计划
### 阶段一基础建设第1-2天
| 任务 | 内容 | 产出 |
|-----|------|-----|
| 关键词整理 | 将本规划的关键词表结构化 | 关键词库CSV文件 |
| 句式模板确认 | 精选20个核心句式 | 句式模板库 |
| 组合脚本编写 | 用Python实现自动组合 | 批量生成脚本 |
### 阶段二批量生成第3-7天
| 天数 | 目标 | 质检标准 |
|-----|------|---------|
| 第3天 | 生成200条初稿 | 句式正确、无语法错误 |
| 第4天 | 生成200条初稿 | 句式正确、无语法错误 |
| 第5天 | 生成200条初稿 | 句式正确、无语法错误 |
| 第6天 | 生成200条初稿 | 句式正确、无语法错误 |
| 第7天 | 生成200条初稿 | 句式正确、无语法错误 |
### 阶段三人工筛选优化第8-10天
| 天数 | 任务 | 标准 |
|-----|------|-----|
| 第8天 | 初筛1000条精品 | 去除重复、逻辑不通 |
| 第9天 | 精修200条重点 | 语言润色、深度强化 |
| 第10天 | 最终校对排版 | 格式统一、风格一致 |
### 阶段四分类整理输出第11-12天
| 任务 | 内容 |
|-----|------|
| 按主题分类 | 按四大主题分区归类 |
| 打标签 | 标记主题、句式、意象 |
| 输出格式 | JSON、CSV、Markdown多格式 |
---
## 六、质量评估标准
### 6.1 五维评分模型
| 维度 | 权重 | 评估要点 |
|-----|:---:|---------|
| 深度 | 25% | 是否有哲理深度,能否引发思考 |
| 共鸣 | 25% | 是否有情感共鸣,让人停留 |
| 简洁 | 20% | 是否言简意赅,无冗余表达 |
| 新颖 | 15% | 是否有独特视角,避免陈词滥调 |
| 完整 | 15% | 是否有头有尾,逻辑自洽 |
### 6.2 淘汰标准
以下情况直接淘汰:
- 语法错误或逻辑不通
- 与已有内容高度重复(相似度>70%
- 过于直白缺乏哲理深度
- 陈词滥调、无新意
- 消极负面、传播负能量
---
## 七、关键词速查表
### 7.1 主主题快速索引
```
A类人生意义系
1 人生 → 旅程、剧本、角色、舞台
2 命运 → 注定、巧合、安排、转折
3 意义 → 价值、方向、目的、追求
4 平凡 → 普通、真实、踏实、日常
5 存在 → 活着、感知、体验、证明
B类情感关系系
6 孤独 → 独处、边缘、清醒、静默
7 爱 → 温暖、刺痛、放手、成全
8 选择 → 路口、承担、割舍、决定
9 关系 → 边界、距离、连接、疏离
10 自由 → 翅膀、天空、解脱、释放
C类成长蜕变系
11 成长 → 蜕变、破茧、代价、成熟
12 痛苦 → 磨砺、锤炼、转化、结晶
13 坚持 → 执着、韧劲、突破、恒心
14 放下 → 释然、接纳、告别、轻松
15 觉醒 → 开悟、看见、醒来、明白
D类世界观系
16 时间 → 流逝、珍惜、永恒、瞬间
17 真相 → 本质、表象、谎言、虚假
18 欲望 → 克制、追求、放下、贪念
19 人性 → 善恶、复杂、真实、面具
20 世界 → 规则、真相、边界、运转
```
### 7.2 句式模板速查
```
对比类:
[1] 真正的____不是____而是____。
[2] 越____越____。
[3] ____从不是____而是____。
因果类:
[4] 当你____你才懂得____。
[5] 只有____过____的人才明白____。
[6] ____的代价是____。
隐喻类:
[7] ____像____终将____。
[8] ____不会说话却教会了我____。
[9] ____是____的镜子照见____。
判断类:
[10] 不怕____只怕____。
[11] ____的尽头是____。
[12] 最深的____往往____。
```
---
## 八、附录100条示例预览
### 人生智慧区(示例)
1. 真正的成熟,不是年龄的增长,而是学会与世界和平相处。
2. 人生没有白走的路,每一步都算数,包括那些弯路。
3. 命运从不亏欠谁,它只是给你想要的,或给你需要的。
4. 平凡不是平庸,而是在平凡中活出真实的自己。
5. 存在不需要证明,活着本身就是最好的证明。
### 情感关系区(示例)
6. 真正的爱,不是占有,而是让对方成为更好的自己。
7. 孤独不是没人爱你,而是你不想爱任何人。
8. 选择一个人,就是选择一种生活方式。
9. 最好的关系,是相处不累,久处不厌。
10. 自由不是想做什么就做什么,而是不想做什么就可以不做什么。
### 成长蜕变区(示例)
11. 成长就是把哭声调成静音的过程。
12. 痛苦是化了妆的祝福,只是当时没有看出来。
13. 坚持不是永不疲倦,而是累了依然选择继续。
14. 放下不是忘记,而是记得但不再疼痛。
15. 觉醒的那一刻,世界还是那个世界,但你已经不一样了。
### 世界观区(示例)
16. 时间从不说话,却回答了所有问题。
17. 真相只有一个,但真相的背面还是真相。
18. 欲望本身没有错,错的是被欲望控制。
19. 人性经不起考验,但值得被理解。
20. 世界从不温柔,但也从不残忍,它只是按规则运转。
---
## 九、总结
通过本规划的实施可以系统化地完成1000条哲理话的生成任务。核心要点
1. **模块化设计**:主主题×子主题×意象×动词×句式的自由组合
2. **量化可控**:明确的配额分配和质量评估标准
3. **高效产出**:自动化组合+人工筛选的混合模式
4. **质量保证**:五维评分模型和淘汰机制
执行过程中可根据实际产出情况动态调整各环节的权重和数量确保最终产出的1000条内容既丰富又深刻。
---
*规划版本v1.0*
*创建时间2026-01-09*
*目标产出1000条哲理话*

View File

@@ -1,6 +1,6 @@
import { PromptGenerator, type PromptGeneratorOptions } from "./prompt-geneator.ts"; import { PromptGenerator, type PromptGeneratorOptions } from "./module/prompt-geneator.ts";
import { writeFile } from "node:fs/promises"; import { writeFile } from "node:fs/promises";
import { Prompt } from "./prompt-perfect.ts"; import { Prompt } from "./module/prompt-perfect.ts";
import { customAlphabet } from "nanoid"; import { customAlphabet } from "nanoid";
const letter = 'abcdefghijklmnopqrstuvwxyz' const letter = 'abcdefghijklmnopqrstuvwxyz'

View File

@@ -1,12 +1,6 @@
import { useConfig } from '@kevisual/use-config'; import { useConfig } from '@kevisual/use-config';
export const config = useConfig(); export const config = useConfig();
export const queueConfig = {
name: 'image-generation-queue',
concurrency: parseInt(config.QUEUE_CONCURRENCY || '1'),
maxFailed: parseInt(config.QUEUE_MAX_FAILED || '2'),
};
export const redisConfig = { export const redisConfig = {
host: config.REDIS_HOST || 'localhost', host: config.REDIS_HOST || 'localhost',
port: parseInt(config.REDIS_PORT || '6379'), port: parseInt(config.REDIS_PORT || '6379'),

View File

@@ -7,4 +7,6 @@ export const logger = new Logger({
export const feishuNotifier = new FeishuNotifier({ export const feishuNotifier = new FeishuNotifier({
webhook: config.FEISHU_NOTIFY_WEBHOOK_URL || '', webhook: config.FEISHU_NOTIFY_WEBHOOK_URL || '',
}); });
export const notify = feishuNotifier;

View File

@@ -0,0 +1,343 @@
import { randomInt as random } from 'es-toolkit';
// ============================================
// 关键词库定义
// ============================================
// 主主题关键词(带子主题)
export const MAIN_THEMES: Record<string, string[]> = {
: ['旅程', '剧本', '角色', '舞台', '起点', '终点', '过程', '意义', '价值', '方向'],
: ['过去', '现在', '未来', '瞬间', '永恒', '季节', '昼夜', '钟表', '沙漏', '流水'],
: ['伤痕', '蜕变', '破茧', '跌倒', '镜子', '旧我', '觉醒', '代价', '忍耐', '成熟'],
: ['独处', '夜晚', '人群疏离', '无人理解', '自我对话', '清醒者', '边缘人', '静默', '影子'],
: ['路口', '岔路', '方向', '代价', '后悔', '坚定', '承担', '割舍', '决定', '权衡'],
: ['翅膀', '天空', '边界', '束缚', '解脱', '随心所欲', '主宰', '释放', '翱翔', '远方'],
: ['伤疤', '火焰', '锤炼', '熔炉', '泪水', '黑夜', '黎明', '破碎', '重建', '结晶'],
: ['温暖', '刺痛', '放手', '成全', '燃烧', '熄灭', '靠近', '远离', '心跳', '温柔'],
: ['终结', '新生', '虚无', '告别', '离开', '遗忘', '铭记', '永恒', '轮回', '起点'],
: ['本质', '表象', '谎言', '虚假', '真实', '假象', '核心', '事实', '表面', '深层'],
: ['克制', '追求', '放下', '贪念', '满足', '渴望', '执念', '放手', '贪婪', '节制'],
: ['执着', '韧劲', '突破', '恒心', '毅力', '不放弃', '继续', '前行', '攀登', '坚守'],
: ['释然', '接纳', '告别', '轻松', '释怀', '松手', '转身', '遗忘', '宽恕', '和解'],
: ['开悟', '看见', '醒来', '明白', '领悟', '理解', '懂得', '发现', '觉悟', '洞悉'],
: ['普通', '真实', '踏实', '日常', '简单', '朴素', '寻常', '平淡', '自然', '安宁'],
: ['价值', '方向', '目的', '追求', '使命', '理由', '答案', '方向感', '存在感', '理由'],
: ['安静', '力量', '倾听', '静默', '无言', '无声', '寂静', '宁静', '平和', '镇定'],
: ['注定', '改变', '巧合', '安排', '转折', '际遇', '天意', '偶然', '必然', '定数'],
: ['善恶', '复杂', '真实', '面具', '伪装', '本质', '脆弱', '坚强', '自私', '无私'],
: ['规则', '真相', '边界', '运转', '秩序', '法则', '规律', '现实', '规则', '边界']
};
// 通用意象关键词
export const IMAGES = [
'光', '暗', '路', '门', '风', '火', '水', '树', '种子', '根', '山', '海', '雾', '雨',
'雪', '霜', '雷', '电', '云', '星', '月', '日', '花', '草', '叶', '果实', '飞鸟',
'游鱼', '蝴蝶', '灯塔', '镜子', '书本', '信', '钟', '钥匙', '锁', '刀', '剑', '帆',
'船', '桥', '窗', '塔', '影', '回声', '脚步', '呼吸', '尘埃', '烟雾', '碎片'
];
// 动词关键词
export const VERBS = [
'燃烧', '沉淀', '穿越', '凝视', '听见', '醒来', '破碎', '重建', '等待', '放手',
'坠落', '升起', '沉默', '挣扎', '回归', '追随', '停留', '遗忘', '铭记', '看清',
'承认', '接受', '抵抗', '屈服', '生长', '凋零', '绽放', '枯萎', '流动', '凝固',
'感受', '体会', '领悟', '明白', '理解', '懂得', '发现', '寻找', '追求', '失去',
'获得', '前行', '停留', '奔跑', '行走', '停留', '守望', '等待', '出发', '到达'
];
// 句式模板(带占位符)
export const TEMPLATES = [
// 对比型
{ type: '对比', text: '真正的{主},不是{子},而是{子}。' },
{ type: '对比', text: '越{子},越{子}。' },
{ type: '对比', text: '{主}与{子},从来不是对立的。' },
{ type: '对比', text: '不是{主}太远,而是{子}太近。' },
{ type: '对比', text: '{主}从不说谎,{子}却常常欺骗。' },
{ type: '对比', text: '不怕{主},只怕{动}。' },
{ type: '对比', text: '{主}从不是{子},而是{意义}。' },
{ type: '对比', text: '最高级的{主},是{动}{意义}。' },
{ type: '对比', text: '最深的{主},往往{动}。' },
{ type: '对比', text: '{主}的尽头,是{动}{意义}。' },
// 因果型
{ type: '因果', text: '当你{动}{主},你才懂得{意义}。' },
{ type: '因果', text: '只有{动}过{主}的人,才明白{意义}。' },
{ type: '因果', text: '{主}教会我,{动}才是{意义}。' },
{ type: '因果', text: '经历过{主},才知道{意义}的可贵。' },
{ type: '因果', text: '{主}的代价,是{动}{意义}。' },
{ type: '因果', text: '因为{动}{主},所以{动}{意义}。' },
{ type: '因果', text: '{主}让我明白,{动}才是真正的{意义}。' },
// 隐喻型
{ type: '隐喻', text: '{主}像{意象},终将{动}。' },
{ type: '隐喻', text: '{意象}不会说话,却教会了我{动}{意义}。' },
{ type: '隐喻', text: '{主}是{意象}的镜子,照见{意义}。' },
{ type: '隐喻', text: '在{主}中,我看见了{意义}。' },
{ type: '隐喻', text: '{主}如同{意象},{动}才是{意义}。' },
{ type: '隐喻', text: '{意象}见证了{主}的{子}。' },
{ type: '隐喻', text: '{主}是{意象},需要{动}才能明白{意义}。' },
{ type: '隐喻', text: '当{意象}遇见{主},{动}才是唯一的{意义}。' },
];
// ============================================
// 生成器类
// ============================================
export interface SentenceGeneratorOptions {
/** 生成数量 */
count?: number;
/** 使用的模板类型 */
templateTypes?: ('对比' | '因果' | '隐喻' | '判断')[];
/** 使用的主主题 */
mainThemes?: string[];
/** 输出格式 */
outputFormat?: 'array' | 'text' | 'json';
/** 是否包含标签 */
withTags?: boolean;
}
export interface GeneratedSentence {
text: string;
theme: string;
template: string;
templateType: string;
tags: string[];
index: number;
}
export class SentenceGenerator {
private options: Required<SentenceGeneratorOptions>;
private themeUsage: Map<string, number> = new Map();
private generatedCount: number = 0;
constructor(options: SentenceGeneratorOptions = {}) {
this.options = {
count: options.count ?? 100,
templateTypes: options.templateTypes ?? ['对比', '因果', '隐喻'],
mainThemes: options.mainThemes ?? Object.keys(MAIN_THEMES),
outputFormat: options.outputFormat ?? 'array',
withTags: options.withTags ?? true
};
}
/**
* 安全获取随机数组元素
*/
private pickRandom<T>(arr: T[]): T | undefined {
if (arr.length === 0) return undefined;
return arr[random(0, arr.length - 1)];
}
/**
* 获取可用主题(考虑均衡使用)
*/
private pickTheme(): string {
const { mainThemes, count } = this.options;
const threshold = count / mainThemes.length * 1.5;
const availableThemes = mainThemes.filter(theme => {
const usage = this.themeUsage.get(theme) || 0;
return usage < threshold;
});
const themes = availableThemes.length > 0 ? availableThemes : mainThemes;
const theme = this.pickRandom(themes) || mainThemes[0];
this.themeUsage.set(theme, (this.themeUsage.get(theme) || 0) + 1);
return theme;
}
/**
* 填充模板
*/
private fillTemplate(template: string, theme: string, subTheme: string, image: string, verb: string, meaning: string): string {
return template
.replace(/\{主\}/g, theme)
.replace(/\{子\}/g, subTheme)
.replace(/\{意象\}/g, image)
.replace(/\{动\}/g, verb)
.replace(/\{意义\}/g, meaning);
}
/**
* 生成单条句子
*/
private generateOne(): GeneratedSentence {
// 选择主主题
const theme = this.pickTheme();
// 选择子主题
const subTheme = this.pickRandom(MAIN_THEMES[theme]) || theme;
// 选择其他关键词
const image = this.pickRandom(IMAGES) || '';
const verb = this.pickRandom(VERBS) || '';
const meaning = this.pickRandom(MAIN_THEMES['意义']) || '意义';
// 选择模板
const availableTemplates = TEMPLATES.filter(t =>
this.options.templateTypes.includes(t.type as any)
);
const templateObj = this.pickRandom(availableTemplates);
if (!templateObj) {
throw new Error('没有可用的模板');
}
// 填充模板
let text = this.fillTemplate(templateObj.text, theme, subTheme, image, verb, meaning);
// 清理多余空格
text = text.replace(/\s+/g, ' ').trim();
this.generatedCount++;
return {
text,
theme,
template: templateObj.text,
templateType: templateObj.type,
tags: [theme, templateObj.type, subTheme, image, verb],
index: this.generatedCount
};
}
/**
* 生成句子
*/
generate(): GeneratedSentence[] {
const results: GeneratedSentence[] = [];
const maxAttempts = this.options.count * 10;
let attempts = 0;
while (results.length < this.options.count && attempts < maxAttempts) {
attempts++;
const sentence = this.generateOne();
// 去重
const isDuplicate = results.some(s => s.text === sentence.text);
if (!isDuplicate) {
results.push(sentence);
}
}
if (results.length < this.options.count) {
console.warn(`只生成了 ${results.length} 条句子,达到最大尝试次数`);
}
return results;
}
/**
* 生成并输出
*/
generateAndOutput(): string | GeneratedSentence[] {
return this.generate();
}
/**
* 导出为CSV
*/
exportToCSV(): string {
const results = this.generate();
const headers = ['序号', '句子', '主题', '模板类型', '标签'];
const rows = results.map(s => [
s.index.toString(),
`"${s.text}"`,
s.theme,
s.templateType,
s.tags.join(';')
]);
return [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
}
/**
* 导出为JSON
*/
exportToJSON(): string {
const results = this.generate();
return JSON.stringify({
meta: {
total: results.length,
generatedAt: new Date().toISOString(),
generator: 'sentence-generator.ts',
version: '1.0'
},
sentences: results
}, null, 2);
}
/**
* 获取统计信息
*/
getStats(): Record<string, number> {
return Object.fromEntries(this.themeUsage);
}
/**
* 重置
*/
reset(): void {
this.generatedCount = 0;
this.themeUsage.clear();
}
}
// ============================================
// 便捷函数
// ============================================
export function generatePhilosophicalSentences(
count: number = 100,
options: Partial<SentenceGeneratorOptions> = {}
): GeneratedSentence[] {
const generator = new SentenceGenerator({ count, ...options });
return generator.generate();
}
export function generateByTheme(theme: string, count: number = 20): GeneratedSentence[] {
const generator = new SentenceGenerator({ count, mainThemes: [theme] });
return generator.generate();
}
export function generateByTemplate(
templateType: '对比' | '因果' | '隐喻' | '判断',
count: number = 50
): GeneratedSentence[] {
const generator = new SentenceGenerator({ count, templateTypes: [templateType] });
return generator.generate();
}
// ============================================
// CLI入口
// ============================================
const isMainModule = process.argv[1]?.includes('sentence-generator');
if (isMainModule) {
const args = process.argv.slice(2);
const count = parseInt(args[0]) || 100;
const format = (args[1] as 'text' | 'json' | 'csv') || 'text';
console.log(`生成 ${count} 条哲理句...\n`);
const generator = new SentenceGenerator({
count,
templateTypes: ['对比', '因果', '隐喻']
});
if (format === 'csv') {
console.log(generator.exportToCSV());
} else if (format === 'json') {
console.log(generator.exportToJSON());
} else {
const sentences = generator.generate();
sentences.forEach(s => {
console.log(`${s.index}. ${s.text} [${s.theme}]`);
});
}
console.log('\n统计信息');
console.log(generator.getStats());
}
export default SentenceGenerator;

View File

@@ -0,0 +1,56 @@
import { Prompt } from "./prompt-perfect.ts"
const exampleData = {
"text": "选择像锁,终将奔跑。",
"theme": "选择",
"template": "{主}像{意象},终将{动}。",
"templateType": "隐喻",
"tags": [
"选择",
"隐喻",
"后悔",
"锁",
"奔跑"
],
"index": 7,
"optimized": "选择如秤,终需掂量。", // 要展示的句子
}
const perfect2 = `你是一个专业的海报设计师和文案策划。请根据以下句子数据,设计一张富有启发性的海报:
**句子信息**
- 主题:{theme}
- 模板:{template}
- 模板类型:{templateType}
- 标签:{tags}
- 待优化句子:{text}
- 推荐展示文案:{optimized}
**设计要求**
1. **文案优化**
- 以 {optimized} 为基础进一步精炼成适合海报展示的短句8-15字
- 语言要有力量感,能引起共鸣
- 可运用比喻、排比等修辞手法增强表现力
2. **意象选择**
- 根据 {tags} 和主题选择1-2个视觉意象
- 意象要能准确传达句子的情感和哲理
- 推荐风格:温暖励志/清新自然/深沉思考/极简有力
3. **视觉风格**
- 现代简约,有质感
- 文字与画面和谐统一
- 适合社交媒体传播
**输出格式**
海报短句:[精炼后的短句]
视觉意象:[1-2个核心意象]
设计风格:[推荐风格]
设计说明:[50字以内的设计思路]
请直接输出以上内容,不要有其他解释。`
export class SentenceImage extends Prompt {
constructor() {
super({ perfectPrompt: perfect2 });
}
}

View File

@@ -0,0 +1,396 @@
import { config } from './config.ts';
import { readFileSync, writeFileSync } from 'node:fs';
import { Kevisual } from '@kevisual/ai';
// ============================================
// AI优化提示词配置
// ============================================
// 通用哲理句优化提示词
export const PERFECT_PROMPT = `你是一个深谙人生哲理的作家。请将以下不完整的哲理句补充完整,使其成为一句发人深省的哲理名言。
要求:
1. 保持原句的核心主题和结构
2. 语言要简洁有力,富有哲理深度
3. 让人读后有所思考和共鸣
4. 符合中文表达习惯
5. 字数控制在20-50字之间
原句:{{sentence}}
请直接输出优化后的句子,不需要任何解释。`;
// 不同主题的专项优化提示词
export const THEME_PROMPTS: Record<string, string> = {
: `你是一个智慧的导师。请将以下关于人生的句子优化成一句深刻的人生感悟:
要求:
- 语言平实却蕴含智慧
- 让人读后有所触动
- 适合作为人生格言
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个洞察时光的智者。请将以下关于时间的句子优化:
要求:
- 体现时间的珍贵与无情
- 让人珍惜当下
- 富有诗意但不晦涩
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个经历过蜕变的智者。请将以下关于成长的句子优化:
要求:
- 体现成长的代价与收获
- 让人对成长有新的理解
- 有力量感但不鸡汤
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个深刻理解孤独的人。请将以下关于孤独的句子优化:
要求:
- 准确描述孤独的本质
- 不是消极,而是清醒
- 让人学会与孤独和解
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个懂得爱的人。请将以下关于爱的句子优化:
要求:
- 体现爱的真谛
- 不是空洞的情话
- 让人对爱有新的认识
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个善于抉择的人。请将以下关于选择的句子优化:
要求:
- 体现选择的重量
- 不是教条,而是智慧
- 让人更慎重地面对选择
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个追求自由的人。请将以下关于自由的句子优化:
要求:
- 准确描述自由的真谛
- 让人理解自由的代价
- 有深度但不晦涩
原句:{{sentence}}
直接输出优化后的句子。`,
: `你是一个从痛苦中走过来的人。请将以下关于痛苦的句子优化:
要求:
- 不是宣泄负面情绪
- 而是让人从痛苦中获得力量
- 转化痛苦为成长
原句:{{sentence}}
直接输出优化后的句子。`
};
// ============================================
// AI客户端接口
// ============================================
export interface PerfectOptions {
/** Kevisual AI 实例 */
ai?: Kevisual;
/** 使用的提示词模板 */
promptTemplate?: string;
/** 并发请求数 */
concurrency?: number;
/** 重试次数 */
retryTimes?: number;
/** 成功后回调 */
onSuccess?: (item: PerfectItem) => void;
/** 失败后回调 */
onError?: (item: PerfectError) => void;
/** 进度回调 */
onProgress?: (progress: ProgressInfo) => void;
}
export interface PerfectItem {
index: number;
text: string;
template: string;
templateType: string;
optimized: string;
prompt?: string;
theme: string;
tags: string[];
}
export interface PerfectError {
index: number;
original: string;
error: string;
theme: string;
}
export interface ProgressInfo {
current: number;
total: number;
percentage: number;
successCount: number;
errorCount: number;
}
// ============================================
// 默认AI实例
// ============================================
function createDefaultAI(): Kevisual {
return new Kevisual({
apiKey: config.KEVISUAL_NEW_API_KEY || '',
});
}
// ============================================
// 句子优化器类
// ============================================
export class SentencePerfect {
private options: Required<PerfectOptions>;
private ai: Kevisual;
constructor(options: PerfectOptions = {}) {
this.ai = options.ai || createDefaultAI();
this.options = {
ai: this.ai,
promptTemplate: options.promptTemplate || PERFECT_PROMPT,
concurrency: options.concurrency ?? 5,
retryTimes: options.retryTimes ?? 3,
onSuccess: options.onSuccess || (() => { }),
onError: options.onError || (() => { }),
onProgress: options.onProgress || (() => { })
};
}
/**
* 获取主题对应的提示词
*/
private getThemePrompt(theme: string, sentence: string): string {
const themePrompt = THEME_PROMPTS[theme];
if (themePrompt) {
return themePrompt.replace('{{sentence}}', sentence);
}
return this.options.promptTemplate.replace('{{sentence}}', sentence);
}
/**
* 优化单条句子(带重试)
*/
async perfectOne(item: PerfectItem): Promise<PerfectItem> {
const prompt = this.getThemePrompt(item.theme, item.text);
let lastError: Error | null = null;
for (let i = 0; i < this.options.retryTimes; i++) {
try {
await this.ai.chat([{ role: 'user', content: prompt }]);
let optimized = this.ai.responseText || '';
optimized = optimized.trim().replace(/^["']|["']$/g, '');
const result: PerfectItem = {
...item,
prompt: prompt,
optimized: optimized || item.text
};
this.options.onSuccess(result);
return result;
} catch (error) {
lastError = error as Error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
const error: PerfectError = {
index: item.index,
original: item.text,
error: lastError?.message || '未知错误',
theme: item.theme
};
this.options.onError(error);
return {
...item,
optimized: item.text
};
}
/**
* 批量优化句子
*/
async perfectBatch(
items: PerfectItem[]
): Promise<PerfectItem[]> {
const total = items.length;
let successCount = 0;
let errorCount = 0;
const results: PerfectItem[] = [];
const batchSize = this.options.concurrency;
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const promises = batch.map(async (item) => {
try {
const result = await this.perfectOne(item);
results.push(result);
successCount++;
} catch {
errorCount++;
}
const current = Math.min(i + batchSize, total);
const percentage = Math.round((current / total) * 100);
this.options.onProgress({
current,
total,
percentage,
successCount,
errorCount
});
});
await Promise.all(promises);
if (i + batchSize < items.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return results;
}
/**
* 从文件加载句子并优化
*/
async perfectFromFile(
inputPath: string,
outputPath: string
): Promise<{ success: number; failed: number; results: any[] }> {
const data = JSON.parse(readFileSync(inputPath, 'utf-8'));
const sentences = data.sentences || data;
const items: PerfectItem[] = sentences.map((s: any, i: number) => ({
index: s.index || i + 1,
text: s.text || s,
template: s.template || '',
templateType: s.templateType || '',
theme: s.theme || '人生',
tags: s.tags || [],
optimized: ''
}));
const results = await this.perfectBatch(items);
// 合并原始数据与优化结果
const mergedResults = results.map((r, i) => {
return {
index: r.index,
text: r.text,
template: r.template,
templateType: r.templateType,
theme: r.theme,
tags: r.tags,
optimized: r.optimized
};
});
const successResults = results.filter(r => r.optimized !== r.text);
const failedResults = results.filter(r => r.optimized === r.text);
const output = {
meta: {
total: results.length,
success: successResults.length,
failed: failedResults.length,
optimizedAt: new Date().toISOString()
},
sentences: mergedResults
};
writeFileSync(outputPath, JSON.stringify(output, null, 2), 'utf-8');
return {
success: successResults.length,
failed: failedResults.length,
results: mergedResults
};
}
/**
* 设置AI实例
*/
setAI(ai: Kevisual): void {
this.ai = ai;
this.options.ai = ai;
}
}
// ============================================
// 便捷函数
// ============================================
/**
* 快速优化单条句子
*/
export async function quickPerfect(
sentence: string,
theme: string = '人生'
): Promise<string> {
const perfect = new SentencePerfect();
const item: PerfectItem = {
index: 0,
text: sentence,
template: '',
templateType: '',
theme,
tags: [],
optimized: ''
};
const result = await perfect.perfectOne(item);
return result.optimized;
}
/**
* 从JSON文件优化句子
*/
export async function perfectFromJSON(
inputPath: string,
outputPath: string,
options: Partial<PerfectOptions> = {}
): Promise<{ success: number; failed: number }> {
const perfect = new SentencePerfect(options);
const result = await perfect.perfectFromFile(inputPath, outputPath);
return { success: result.success, failed: result.failed };
}
export default SentencePerfect;

View File

@@ -0,0 +1,74 @@
import { app, ossService, pbService, redis } from '@/app.ts'
import { addImageGenerateJob } from '@/task/image-creator.job.ts';
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
type SentenceItem = {
text: string;
theme: string;
template: string;
templateType: string;
tags: string[];
index: number;
prompt?: string;
optimized?: string;
}
app.route({
path: 'image-creator',
key: 'create-sentence-list',
description: '导入句子列表',
middleware: ['auth']
}).define(async (ctx) => {
const data: SentenceItem[] = ctx.query?.data || [];
if (!Array.isArray(data) || data.length === 0) {
ctx.throw(400, 'Invalid input data');
}
const BATCH_SIZE = 50;
for (let i = 0; i < data.length; i += BATCH_SIZE) {
const chunk = data.slice(i, i + BATCH_SIZE);
const batch = pbService.client.createBatch();
for (const item of chunk) {
batch.collection(pbService.collectionName).create({
text: '句子生成海报',
summary: item.optimized,
description: '',
status: '创建',
tags: ['sentence', ...item.tags],
data: {
sentence: item
}
});
}
await batch.send();
console.log(`Processed batch ${Math.floor(i / BATCH_SIZE) + 1}: ${chunk.length} items.`);
await sleep(200); // 避免短时间内添加过多请求
}
ctx.body = { message: `Imported ${data.length} sentences.` };
}).addTo(app);
app.route({
path: 'image-creator',
key: 'fix-sentences',
middleware: ['auth'],
description: '修正句子条目的 summary 字段'
}).define(async () => {
const list = await pbService.collection.getFullList({
filter: `tags~"sentence"`,
})
console.log(`Fetched ${list.length} sentence items from PocketBase.`);
const BATCH_SIZE = 50;
for (let i = 0; i < list.length; i += BATCH_SIZE) {
const chunk = list.slice(i, i + BATCH_SIZE);
const batch = pbService.client.createBatch();
for (const item of chunk) {
const sentenceData = item.data?.sentence || {};
if (!sentenceData.optimized) continue;
batch.collection(pbService.collectionName).update(item.id, {
summary: sentenceData.optimized,
});
}
await batch.send();
console.log(`Processed batch ${Math.floor(i / BATCH_SIZE) + 1}: ${chunk.length} items.`);
}
}).addTo(app);

View File

@@ -1,15 +1,43 @@
import { app, ossService, pbService, redis } from '@/app.ts' import { app, ossService, pbService, redis } from '@/app.ts'
import { addImageGenerateJob } from '@/task/image-creator.job.ts'; import { addImageGenerateJob } from '@/task/image-creator.job.ts';
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
app.route({ app.route({
path: 'image-creator', path: 'image-creator',
key: 'create-task', key: 'create-task',
description: '创建图片生成任务,将所有计划中的任务加入图片生成队列',
middleware: ['auth']
}).define(async (ctx) => { }).define(async (ctx) => {
const status = ctx.status ?? '计划中';
const list = await pbService.collection.getFullList({ const list = await pbService.collection.getFullList({
filter: 'status="计划中"', filter: `status="${status}"`,
}) })
for (const item of list) { for (const item of list) {
await addImageGenerateJob(item); await addImageGenerateJob(item);
await sleep(100); // 避免短时间内添加过多任务
} }
console.log(`Added ${list.length} image generate jobs to the queue.`); console.log(`Added ${list.length} image generate jobs to the queue.`);
}).addTo(app);
app.route({
path: 'image-creator',
key: 'batch-update-tags',
description: '测试 Redis 连接',
isDebug: true
}).define(async (ctx) => {
const list = await pbService.collection.getFullList({
filter: `tags="[]"`,
})
console.log(`Fetched ${list.length} items from PocketBase.`, list[0]);
const BATCH_SIZE = 50;
for (let i = 0; i < list.length; i += BATCH_SIZE) {
const chunk = list.slice(i, i + BATCH_SIZE);
const batch = pbService.client.createBatch();
for (const item of chunk) {
batch.collection(pbService.collectionName).update(item.id, { tags: ["horse"] });
}
await batch.send();
console.log(`Processed batch ${Math.floor(i / BATCH_SIZE) + 1}: ${chunk.length} items.`);
}
ctx.body = { message: `Processed ${list.length} items.` };
}).addTo(app); }).addTo(app);

View File

@@ -0,0 +1,69 @@
import { pbService, jimengService, ossService, app } from '../index.ts';
import type { ImageCollection } from '../services/pb.service.ts';
// 更新 PB 状态
export async function updateItemStatus(
itemId: string,
status: string,
extraData?: Partial<ImageCollection>
): Promise<void> {
const collection = pbService.getCollection<ImageCollection>(pbService.collectionName);
if (extraData) {
const existingItem = await pbService.collection.getOne(itemId);
const data = existingItem.data;
const existingImages = data?.images || [];
const newImages = extraData.data?.images || [];
await collection.update(itemId, {
status,
...extraData,
data: {
...extraData?.data,
...data,
images: [...existingImages, ...newImages],
},
});
} else {
await collection.update(itemId, {
status,
});
}
}
app.route({
path: 'image-creator',
key: 'image-update',
description: '更新所有图片生成任务的状态',
middleware: ['auth']
}).define(async (ctx) => {
const { itemId, status, extraData } = ctx.query;
if (!itemId || !status) {
ctx.throw(400, 'Missing itemId or status');
}
await updateItemStatus(itemId as string, status as string, extraData as Partial<ImageCollection> | undefined);
ctx.body = { message: 'Item status updated successfully' };
}).addTo(app);
app.route({
path: 'image-creator',
key: 'image-download',
description: '下载所有图片生成任务的图片,并上传到 OSS, 返回图片链接。参数: itemId, imageUrl, index',
middleware: ['auth']
}).define(async (ctx) => {
const { itemId, imageUrl, index = 0 } = ctx.query;
if (!itemId || !imageUrl || index === undefined) {
ctx.throw(400, 'Missing itemId, imageUrl or index');
}
const imageBuffer = await jimengService.downloadImage(imageUrl);
const filename = `generated_${itemId}_${index}_${Date.now()}.png`;
await ossService.putObject(filename, imageBuffer);
const ossUrl = ossService.getLink(filename)
console.log(`[ImageDownload] Image uploaded to OSS: ${ossUrl}`);
const imageData = { type: 'tos' as const, url: ossUrl };
await updateItemStatus(itemId as string, 'completed', {
data: {
images: [imageData],
},
});
ctx.body = { message: 'Image downloaded and uploaded to OSS successfully', ossUrl };
}).addTo(app);

View File

@@ -1 +1,14 @@
import './create-task.ts' import { app } from '../index.ts';
import './create-task.ts'
import './image-update.ts'
import './create-sentence.ts'
app.route({
path: 'auth',
id: 'auth'
}).define(async (ctx) => {
// ctx.body = { message: 'Auth route' };
}).addTo(app);

View File

@@ -17,13 +17,119 @@ export class PBCore {
} }
} }
async loginAdmin(email: string, password: string) { async loginByPassword(email: string, password: string, admin: boolean = true) {
const authData = await this.client.collection("_superusers").authWithPassword(email, password); const collectionName = admin ? '_superusers' : 'users';
const authData = await this.client.collection(collectionName).authWithPassword(email, password);
this.emitter.emit('login', authData); this.emitter.emit('login', authData);
console.log('PocketBase admin logged in:', authData);
return authData; return authData;
} }
async login({ username, password, admin = true }: { username: string; password: string, admin?: boolean }) {
if (this.client.authStore.isValid) {
return true;
}
try {
await this.loginByPassword(username, password, admin);
return true;
} catch (error) {
console.error('PocketBase login error:', error);
return false;
}
}
/**
*
* https://pocketbase.io/docs/api-collections/#create-collection
*
* */
async createCollection({ name, type, fields }: CreateCollection) {
const collections = await this.client.collections.getFullList();
const existing = collections.find(c => c.name === name);
if (existing) {
const collection = this.client.collection(name);
const schema = await this.client.collections.getOne(name);
return {
schema: schema,
existing: true,
collection
};
}
const schema = await this.client.collections.create({
name,
type: type ?? 'base',
fields: fields ?? defaultFields,
});
const collection = this.client.collection(name);
return {
collection,
schema
}
}
} }
export const defaultFields: CollectionFields[] = [
{
name: 'title',
type: 'text',
},
{
name: 'summary',
type: 'text',
},
{
name: 'description',
type: 'text',
},
{
name: 'tags',
type: 'json',
},
{
name: 'data',
type: 'json',
},
{
name: 'status',
type: 'text',
},
{
name: 'created',
type: 'autodate',
onCreate: true,
},
{
name: 'updated',
type: 'autodate',
onCreate: true,
onUpdate: true,
}
]
export type CollectionFields = {
name: string;
type: string;
required?: boolean;
options?: any;
onCreate?: boolean;
onUpdate?: boolean;
}
export type CreateCollectioRule = {
listRule?: string;
viewRule?: string;
/**
* createRule: 'id = @request.auth.id',
*/
createRule?: string;
updateRule?: string;
deleteRule?: string;
}
export type CreateCollection = {
name: string;
type?: 'base' | 'viwer' | 'auth';
fields?: CollectionFields[];
/**
* viewer:
* viewQuery: 'SELECT id, name from posts',
*/
viewQuery?: string;
} & CreateCollectioRule;
export class PBService extends PBCore { export class PBService extends PBCore {
collectionName = 'images_generation_tasks'; collectionName = 'images_generation_tasks';
@@ -51,10 +157,10 @@ export class PBService extends PBCore {
const ImageTaskStatus = ['提示词优化中', '计划中', '生成图片中', '图片下载中', '暂停中', '已完成', '失败'] as const; const ImageTaskStatus = ['提示词优化中', '计划中', '生成图片中', '图片下载中', '暂停中', '已完成', '失败'] as const;
type Data = { type Data<T = {}> = {
images: { type: 'jimeng' | 'tos', url: string }[]; images: { type: 'jimeng' | 'tos', url: string }[];
} } & T
export type ImageCollection = { export type ImageCollection<T = any> = {
id: string; id: string;
created: string; created: string;
updated: string; updated: string;
@@ -62,6 +168,6 @@ export type ImageCollection = {
tags: any; tags: any;
summary: string; summary: string;
description: string; description: string;
data: Data; data: Data<T>;
status: typeof ImageTaskStatus[number]; status: typeof ImageTaskStatus[number];
} }

View File

@@ -1,7 +1,9 @@
import { Worker, Queue, Job } from 'bullmq'; import { Worker, Queue, Job } from 'bullmq';
import { getRedisConnection } from '../module/redis.ts'; import { getRedisConnection } from '../module/redis.ts';
import { pbService, jimengService, ossService } from '../index.ts'; import { pbService, jimengService, ossService, app } from '../index.ts';
import type { ImageCollection } from '../services/pb.service.ts'; import type { ImageCollection } from '../services/pb.service.ts';
import { updateItemStatus } from '../routes/image-update.ts';
import { notify } from '@/module/logger.ts';
export const IMAGE_CREATOR_JOB = 'image-creator'; export const IMAGE_CREATOR_JOB = 'image-creator';
export const IMAGE_GENERATE_JOB = 'image-generate'; export const IMAGE_GENERATE_JOB = 'image-generate';
@@ -44,34 +46,6 @@ export interface ImageDownloadJobData {
index: number; index: number;
} }
// 更新 PB 状态
async function updateItemStatus(
itemId: string,
status: string,
extraData?: Partial<ImageCollection>
): Promise<void> {
const collection = pbService.getCollection<ImageCollection>(pbService.collectionName);
if (extraData) {
const existingItem = await pbService.collection.getOne(itemId);
const data = existingItem.data;
const existingImages = data?.images || [];
const newImages = extraData.data?.images || [];
await collection.update(itemId, {
status,
...extraData,
data: {
...extraData?.data,
...data,
images: [...existingImages, ...newImages],
},
});
} else {
await collection.update(itemId, {
status,
});
}
}
/** /**
* 单独添加生成图片任务 * 单独添加生成图片任务
@@ -183,6 +157,7 @@ export async function runImageDownloadWorker(): Promise<void> {
worker.on('failed', (job, err) => { worker.on('failed', (job, err) => {
console.error(`[ImageDownload] Job failed: ${job?.id}, error: ${err.message}`); console.error(`[ImageDownload] Job failed: ${job?.id}, error: ${err.message}`);
notify.notify(`[ImageDownload] \nJob failed: ${job?.id}, error: ${err.message}\n Job data: ${JSON.stringify(job?.data)}`);
}); });
console.log('[ImageDownload] Worker started'); console.log('[ImageDownload] Worker started');
@@ -245,6 +220,7 @@ export async function runImageGenerateWorker(): Promise<void> {
worker.on('failed', (job, err) => { worker.on('failed', (job, err) => {
console.error(`[ImageGenerate] Job failed: ${job?.id}, error: ${err.message}`); console.error(`[ImageGenerate] Job failed: ${job?.id}, error: ${err.message}`);
notify.notify(`[ImageGenerate] \nJob failed: ${job?.id}, error: ${err.message}\n Job data: ${JSON.stringify(job?.data)}`);
}); });
console.log('[ImageGenerate] Worker started'); console.log('[ImageGenerate] Worker started');

View File

@@ -2,15 +2,16 @@ import { Worker, Job } from 'bullmq';
import { getRedisConnection } from '../module/redis.ts'; import { getRedisConnection } from '../module/redis.ts';
import { Prompt, pbService, ai } from '../index.ts'; import { Prompt, pbService, ai } from '../index.ts';
import type { ImageCollection } from '../services/pb.service.ts'; import type { ImageCollection } from '../services/pb.service.ts';
// 重新导出 Queue因为需要在 addPerfectPromptJob 中使用 import { updateItemStatus } from '../routes/image-update.ts';
import { Queue } from 'bullmq'; import { Queue } from 'bullmq';
import { notify } from '@/module/logger.ts';
export const PERFECT_PROMPT_JOB = 'perfect-prompt'; export const PERFECT_PROMPT_JOB = 'perfect-prompt';
// 状态常量 // 状态常量
export const PerfectPromptStatus = { export const PerfectPromptStatus = {
PENDING: '提示词优化中' as const, PENDING: '提示词优化中' as const,
COMPLETED: '已完成' as const, PLANNING: '计划中' as const,
FAILED: '失败' as const, FAILED: '失败' as const,
}; };
@@ -34,31 +35,6 @@ const DEFAULT_PERFECT_PROMPT = `请你将以下提示词进行完善,使其更
7. 使用中文进行描述。 7. 使用中文进行描述。
`; `;
// 更新 PB 状态
async function updateItemStatus(
itemId: string,
status: string,
extraData?: Partial<ImageCollection>
): Promise<void> {
const collection = pbService.getCollection<ImageCollection>(pbService.collectionName);
if (extraData) {
const existingItem = await pbService.collection.getOne(itemId);
const data = existingItem.data;
await collection.update(itemId, {
status,
...extraData,
data: {
...extraData?.data,
...data,
},
});
} else {
await collection.update(itemId, {
status,
});
}
}
/** /**
* 单独添加优化提示词任务 * 单独添加优化提示词任务
*/ */
@@ -118,7 +94,7 @@ export async function runPerfectPromptWorker(): Promise<void> {
console.log(`[PerfectPrompt] Perfect prompt generated for item: ${itemId}`); console.log(`[PerfectPrompt] Perfect prompt generated for item: ${itemId}`);
// 更新状态为已完成,并保存优化后的提示词 // 更新状态为已完成,并保存优化后的提示词
await updateItemStatus(itemId, PerfectPromptStatus.COMPLETED, { await updateItemStatus(itemId, PerfectPromptStatus.PLANNING, {
description: perfectText, description: perfectText,
}); });
@@ -146,6 +122,7 @@ export async function runPerfectPromptWorker(): Promise<void> {
worker.on('failed', (job, err) => { worker.on('failed', (job, err) => {
console.error(`[PerfectPrompt] Job failed: ${job?.id}, error: ${err.message}`); console.error(`[PerfectPrompt] Job failed: ${job?.id}, error: ${err.message}`);
notify.notify(`[PerfectPrompt] \nJob failed: ${job?.id}, error: ${err.message}\n Job data: ${JSON.stringify(job?.data)}`);
}); });
console.log('[PerfectPrompt] Worker started'); console.log('[PerfectPrompt] Worker started');

View File

@@ -4,9 +4,17 @@ export {
app app
} }
const res = await app.run({ // const res = await app.run({
path: 'image-creator', // path: 'image-creator',
key: 'create-task', // key: 'create-task',
}) // })
console.log('Route run result:', res) // console.log('Route run result:', res)
// const res = await app.run({
// path: 'image-creator',
// key: 'batch-update-tags',
// })
// console.log('Route run result:', res)

View File

@@ -0,0 +1,57 @@
import { SentenceGenerator } from '../src/module/sentence-generator.ts';
import { writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const outputPath = join(__dirname, '../data/sentence-01.json');
console.log('🚀 开始生成1000条哲理句...\n');
const generator = new SentenceGenerator({
count: 1000,
outputFormat: 'json',
withTags: true,
templateTypes: ['对比', '因果', '隐喻', '判断']
});
const results = generator.generateAndOutput() as any[];
// 添加元信息
const outputData = {
meta: {
total: results.length,
generatedAt: new Date().toISOString(),
generator: 'sentence-generator.ts',
version: '1.0'
},
sentences: results
};
// 写入文件
writeFileSync(outputPath, JSON.stringify(outputData, null, 2), 'utf-8');
console.log(`✅ 成功生成 ${results.length} 条句子`);
console.log(`📁 输出到: ${outputPath}\n`);
// 打印统计信息
console.log('📊 主题分布统计:');
const stats = generator.getStats();
const sortedStats = Object.entries(stats)
.sort(([, a], [, b]) => b - a)
.slice(0, 10);
sortedStats.forEach(([theme, count]) => {
const bar = '█'.repeat(Math.floor(count / 10));
console.log(` ${theme.padEnd(6)}: ${count.toString().padStart(4)} ${bar}`);
});
console.log(`\n📋 模板类型分布:`);
const templateCount: Record<string, number> = {};
results.forEach((s: any) => {
const type = s.template.split(/[\d]/)[0] || s.template;
templateCount[type] = (templateCount[type] || 0) + 1;
});
Object.entries(templateCount).forEach(([type, count]) => {
console.log(` ${type}: ${count}`);
});

View File

@@ -0,0 +1,24 @@
import { app } from './common.ts';
import fs from 'node:fs';
import path from 'node:path';
const sentence = path.join(process.cwd(), 'data', 'sentence-01-optimized.json');
const data = JSON.parse(fs.readFileSync(sentence, 'utf-8'));
async function run() {
const sentences = data?.sentences || [];
console.log(`Importing ${sentences.length} sentences...`);
const res = await app.run({
path: 'image-creator',
// key: 'create-sentence-list',
key: 'fix-sentences',
payload: {
data: sentences
}
});
console.log('Import sentence result:', res);
}
await run();

View File

@@ -34,10 +34,33 @@ async function main() {
// await sleep(100); // To avoid hitting rate limits // await sleep(100); // To avoid hitting rate limits
// } // }
const list = await pbService.collection.getFullList({ // const list = await pbService.collection.getFullList({
sort: '-created', // sort: '-created',
fields: 'id,title,summary,description,tags,status', // fields: 'id,title,summary,description,tags,status',
}) // })
console.log('PocketBase Records:', list.length); // console.log('PocketBase Records:', list.length);
// const b = await pbService.client.collections.create({
// name: 'exampleBase',
// type: 'base',
// fields: [
// {
// name: 'title',
// type: 'text',
// required: true,
// min: 10,
// },
// {
// name: 'status',
// type: 'bool',
// },
// ],
// });
// console.log('Created collection:', b);
const c = await pbService.createCollection({
name: 'exampleBase',
});
console.log('Created collection via PBService:', c);
} }
main(); main();

View File

@@ -0,0 +1,48 @@
import { ai } from '../src/index.ts'
import { SentenceImage } from '../src/module/sentence-image.ts';
export async function generatePerfectImage() {
const sentenceImage = new SentenceImage();
// const content = JSON.stringify({
// "text": "选择像锁,终将奔跑。",
// "theme": "选择",
// "template": "{主}像{意象},终将{动}。",
// "templateType": "隐喻",
// "tags": [
// "选择",
// "隐喻",
// "后悔",
// "锁",
// "奔跑"
// ],
// "index": 7,
// "optimized": "选择如秤,终需掂量。"
// });
const content = JSON.stringify({
"text": "人生如逆旅,我亦是行人。",
"theme": "人生",
"template": "人生如{意象},我亦是{身份}。",
"templateType": "隐喻",
"tags": [
"人生",
"隐喻",
"旅途",
"行人"
],
"index": 1,
"optimized": "人生似长河,我自是过客。"
});
const prompt = sentenceImage.perfect(content);
const response = await ai.chat([], {
messages: [{ role: "user", content: prompt }],
// model: 'qwen-turbo',
// model: 'doubao-seed-1-6-251015',
model: 'qwen-plus',
enable_thinking: true
})
console.log('生成的海报设计方案:\n', response.choices[0].message.content);
return response.choices[0].message.content;
}
generatePerfectImage()

View File

@@ -0,0 +1,90 @@
import { SentencePerfect } from '../src/module/sentence-perfect.ts';
import { readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Kevisual } from '@kevisual/ai';
import { config } from '../src/module/config.ts';
const __dirname = dirname(fileURLToPath(import.meta.url));
// 配置
const INPUT_PATH = join(__dirname, '../data/sentence-01.json');
const OUTPUT_PATH = join(__dirname, '../data/sentence-01-optimized.json');
// 读取原始句子
const data = JSON.parse(readFileSync(INPUT_PATH, 'utf-8'));
const sentences = data.sentences || data;
console.log(`📖 加载了 ${sentences.length} 条句子\n`);
// 创建 AI 实例
const ai = new Kevisual({
apiKey: config.KEVISUAL_NEW_API_KEY || '',
model: 'qwen-turbo'
});
// 创建优化器
const perfect = new SentencePerfect({
ai,
concurrency: 3, // 并发数
retryTimes: 2, // 重试次数
// 进度回调
onProgress: (progress) => {
const { current, total, percentage, successCount, errorCount } = progress;
process.stdout.write(`\r🔄 进度: ${current}/${total} (${percentage}%) | 成功: ${successCount} | 失败: ${errorCount}`);
},
// 成功回调
onSuccess: (result) => {
if (result.index % 10 === 0) {
console.log(`\n✅ ${result.index}: "${result.text}"`);
console.log(` → "${result.optimized}"`);
}
},
// 失败回调
onError: (error) => {
console.log(`\n❌ ${error.index}: ${error.error}`);
}
});
// 准备数据 - 只处理前20条作为测试
// const items = sentences.slice(0, 2).map((s: any, i: number) => ({
// ...s,
// index: s.index || i + 1
// }));
const items = sentences;
console.log('🚀 开始优化句子...\n');
// 执行优化
const startTime = Date.now();
const results = await perfect.perfectBatch(items);
const elapsed = Date.now() - startTime;
console.log(`\n\n✅ 完成!耗时: ${(elapsed / 1000).toFixed(2)}`);
// 统计
const successResults = results.filter(r => r.optimized !== r.text);
console.log(` 成功: ${successResults.length}/${results.length}`);
// 保存结果
const output = {
meta: {
total: results.length,
success: successResults.length,
optimizedAt: new Date().toISOString()
},
sentences: results
};
writeFileSync(OUTPUT_PATH, JSON.stringify(output, null, 2), 'utf-8');
console.log(`📁 结果已保存到: ${OUTPUT_PATH}`);
// 显示示例
console.log('\n📝 优化示例:');
successResults.slice(0, 5).forEach((r, i) => {
console.log(`\n${i + 1}. [${r.theme}]`);
console.log(` 原: ${r.text}`);
console.log(` 优: ${r.optimized}`);
});