472bf8b732
Co-Authored-By: Codex <noreply@openai.com>
286 lines
9.3 KiB
Go
286 lines
9.3 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/basketikun/infinite-canvas/model"
|
|
"github.com/basketikun/infinite-canvas/repository"
|
|
)
|
|
|
|
const (
|
|
gptImage2RawBase = "https://raw.githubusercontent.com/EvoLinkAI/awesome-gpt-image-2-API-and-Prompts/main"
|
|
awesomeGptImageRawBase = "https://raw.githubusercontent.com/ZeroLu/awesome-gpt-image/main"
|
|
awesomeGpt4oImagePromptsBase = "https://raw.githubusercontent.com/ImgEdify/Awesome-GPT4o-Image-Prompts/main"
|
|
youMindGptImage2RawBase = "https://raw.githubusercontent.com/YouMind-OpenLab/awesome-gpt-image-2/main"
|
|
youMindNanoBananaProRawBase = "https://raw.githubusercontent.com/YouMind-OpenLab/awesome-nano-banana-pro-prompts/main"
|
|
)
|
|
|
|
var gptImage2CaseFiles = []string{"README.md", "cases/ad-creative.md", "cases/character.md", "cases/comparison.md", "cases/ecommerce.md", "cases/portrait.md", "cases/poster.md", "cases/ui.md"}
|
|
|
|
type gptImage2Data struct {
|
|
Records []struct {
|
|
Title string `json:"title"`
|
|
TweetURL string `json:"tweet_url"`
|
|
ImageDir string `json:"image_dir"`
|
|
Category string `json:"category"`
|
|
AddedAt string `json:"added_at"`
|
|
} `json:"records"`
|
|
}
|
|
|
|
func SyncPromptCategory(category string) ([]model.PromptCategory, error) {
|
|
for _, item := range repository.PromptCategories() {
|
|
if item.Category != category {
|
|
continue
|
|
}
|
|
items, err := buildPromptCategory(item.Category)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := repository.ReplacePromptCategory(item, items); err != nil {
|
|
return nil, err
|
|
}
|
|
return repository.ListPromptCategories()
|
|
}
|
|
return nil, errors.New("未知提示词分类")
|
|
}
|
|
|
|
func buildPromptCategory(category string) ([]model.Prompt, error) {
|
|
switch category {
|
|
case "gpt-image-2-prompts":
|
|
return buildGptImage2Prompts()
|
|
case "awesome-gpt-image":
|
|
return buildAwesomeGptImagePrompts()
|
|
case "awesome-gpt4o-image-prompts":
|
|
return buildAwesomeGpt4oImagePrompts()
|
|
case "youmind-gpt-image-2":
|
|
return buildYouMindGptImage2Prompts()
|
|
case "youmind-nano-banana-pro":
|
|
return buildYouMindNanoBananaProPrompts()
|
|
}
|
|
return nil, errors.New("未知提示词分类")
|
|
}
|
|
|
|
func fetchText(baseURL, file string) (string, error) {
|
|
request, _ := http.NewRequest(http.MethodGet, baseURL+"/"+file, nil)
|
|
client := http.Client{Timeout: 30 * time.Second}
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return "", errors.New(file + " 拉取失败")
|
|
}
|
|
data, err := io.ReadAll(response.Body)
|
|
return string(data), err
|
|
}
|
|
|
|
func buildGptImage2Prompts() ([]model.Prompt, error) {
|
|
cases := map[string]string{}
|
|
raw, err := fetchText(gptImage2RawBase, "data/ingested_tweets.json")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data := gptImage2Data{}
|
|
if err := json.Unmarshal([]byte(raw), &data); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, file := range gptImage2CaseFiles {
|
|
markdown, err := fetchText(gptImage2RawBase, file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
collectGptImage2Cases(cases, markdown)
|
|
}
|
|
items := []model.Prompt{}
|
|
for _, item := range data.Records {
|
|
prompt := cases[item.TweetURL]
|
|
if prompt == "" {
|
|
continue
|
|
}
|
|
image := gptImage2RawBase + "/" + item.ImageDir + "/output.jpg"
|
|
items = append(items, model.Prompt{ID: "gpt-image-2-prompts-" + leftPad(len(items)+1), Title: item.Title, CoverURL: image, Prompt: prompt, Tags: tagsFromCategory(item.Category), CreatedAt: item.AddedAt, UpdatedAt: item.AddedAt, Preview: markdownPreview([]string{image})})
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func collectGptImage2Cases(cases map[string]string, markdown string) {
|
|
re := regexp.MustCompile("(?s)### Case \\d+: \\[[^\\]]+\\]\\(([^)]+)\\).*?\\*\\*Prompt:\\*\\*\\s*\\r?\\n\\s*```[\\w-]*\\r?\\n(.*?)\\r?\\n```")
|
|
for _, match := range re.FindAllStringSubmatch(markdown, -1) {
|
|
cases[match[1]] = strings.TrimSpace(match[2])
|
|
}
|
|
}
|
|
|
|
func buildAwesomeGptImagePrompts() ([]model.Prompt, error) {
|
|
markdown, err := fetchText(awesomeGptImageRawBase, "README.zh-CN.md")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := []model.Prompt{}
|
|
for _, section := range splitBeforeHeading(markdown, "## ") {
|
|
tags := tagsFromHeading(firstMatch(section, `(?m)^##\s+(.+)$`))
|
|
for _, block := range splitBeforeHeading(section, "### ") {
|
|
title := strings.TrimSpace(regexp.MustCompile(`\[([^\]]+)]\([^)]+\)`).ReplaceAllString(firstMatch(block, `(?m)^###\s+(.+)$`), "$1"))
|
|
prompt := strings.TrimSpace(firstMatch(block, "(?s)\\*\\*提示词:\\*\\*\\s*\\r?\\n\\s*```[\\w-]*\\r?\\n(.*?)\\r?\\n```"))
|
|
if title == "" || prompt == "" {
|
|
continue
|
|
}
|
|
images := extractMarkdownImages(awesomeGptImageRawBase, block)
|
|
cover := ""
|
|
if len(images) > 0 {
|
|
cover = images[0]
|
|
}
|
|
items = append(items, model.Prompt{ID: "awesome-gpt-image-" + leftPad(len(items)+1), Title: title, CoverURL: cover, Prompt: prompt, Tags: tags, Preview: markdownPreview(images)})
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func buildAwesomeGpt4oImagePrompts() ([]model.Prompt, error) {
|
|
markdown, err := fetchText(awesomeGpt4oImagePromptsBase, "README.zh-CN.md")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := []model.Prompt{}
|
|
for _, block := range splitBeforeHeading(markdown, "### ") {
|
|
title := strings.TrimSpace(firstMatch(block, `(?m)^###\s+(.+)$`))
|
|
prompt := strings.TrimSpace(firstMatch(block, "(?s)- \\*\\*提示词文本:\\*\\*\\s*`(.*?)`"))
|
|
if title == "" || prompt == "" {
|
|
continue
|
|
}
|
|
images := extractMarkdownImages(awesomeGpt4oImagePromptsBase, block)
|
|
cover := ""
|
|
if len(images) > 0 {
|
|
cover = images[0]
|
|
}
|
|
items = append(items, model.Prompt{ID: "awesome-gpt4o-image-prompts-" + leftPad(len(items)+1), Title: title, CoverURL: cover, Prompt: prompt, Tags: []string{"gpt4o"}, Preview: markdownPreview(images)})
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func buildYouMindGptImage2Prompts() ([]model.Prompt, error) {
|
|
return buildYouMindPrompts(youMindGptImage2RawBase, "youmind-gpt-image-2", "gpt-image-2")
|
|
}
|
|
|
|
func buildYouMindNanoBananaProPrompts() ([]model.Prompt, error) {
|
|
return buildYouMindPrompts(youMindNanoBananaProRawBase, "youmind-nano-banana-pro", "nano-banana-pro")
|
|
}
|
|
|
|
func buildYouMindPrompts(baseURL, idPrefix, modelTag string) ([]model.Prompt, error) {
|
|
markdown, err := fetchText(baseURL, "README_zh.md")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := []model.Prompt{}
|
|
for _, block := range splitBeforeHeading(markdown, "### ") {
|
|
title := strings.TrimSpace(firstMatch(block, `(?m)^###\s+No\.\s*\d+:\s*(.+)$`))
|
|
prompt := strings.TrimSpace(firstMatch(block, "(?s)#### .*?提示词\\s*\\r?\\n\\s*```[\\w-]*\\r?\\n(.*?)\\r?\\n```"))
|
|
if title == "" || prompt == "" {
|
|
continue
|
|
}
|
|
images := extractMarkdownImages(baseURL, block)
|
|
cover := ""
|
|
if len(images) > 0 {
|
|
cover = images[0]
|
|
}
|
|
items = append(items, model.Prompt{ID: idPrefix + "-" + leftPad(len(items)+1), Title: title, CoverURL: cover, Prompt: prompt, Tags: youMindTags(title, modelTag), Preview: markdownPreview(images)})
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func splitBeforeHeading(markdown string, prefix string) []string {
|
|
blocks := []string{}
|
|
lines := strings.Split(markdown, "\n")
|
|
current := []string{}
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, prefix) && len(current) > 0 {
|
|
blocks = append(blocks, strings.Join(current, "\n"))
|
|
current = []string{}
|
|
}
|
|
current = append(current, line)
|
|
}
|
|
return append(blocks, strings.Join(current, "\n"))
|
|
}
|
|
|
|
func firstMatch(value string, pattern string) string {
|
|
match := regexp.MustCompile(pattern).FindStringSubmatch(value)
|
|
if len(match) > 1 {
|
|
return match[1]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func tagsFromCategory(category string) []string {
|
|
return splitTags(regexp.MustCompile(`(?i)\s+Cases$`).ReplaceAllString(category, ""), `\s*(&|and)\s*`)
|
|
}
|
|
|
|
func tagsFromHeading(heading string) []string {
|
|
return splitTags(regexp.MustCompile(`[^\p{L}\p{N}/&、与 ]`).ReplaceAllString(heading, ""), `\s*(/|&|、|与)\s*`)
|
|
}
|
|
|
|
func youMindTags(title, modelTag string) []string {
|
|
tags := []string{modelTag}
|
|
parts := strings.SplitN(title, " - ", 2)
|
|
if len(parts) > 1 {
|
|
tags = append(tags, tagsFromHeading(parts[0])...)
|
|
}
|
|
return tags
|
|
}
|
|
|
|
func splitTags(value string, pattern string) []string {
|
|
tags := []string{}
|
|
for _, tag := range regexp.MustCompile(pattern).Split(value, -1) {
|
|
if tag = strings.ToLower(strings.TrimSpace(tag)); tag != "" {
|
|
tags = append(tags, tag)
|
|
}
|
|
}
|
|
return tags
|
|
}
|
|
|
|
func markdownPreview(images []string) string {
|
|
lines := []string{}
|
|
for _, image := range images {
|
|
if image != "" {
|
|
lines = append(lines, "")
|
|
}
|
|
}
|
|
return strings.Join(lines, "\n\n")
|
|
}
|
|
|
|
func extractMarkdownImages(baseURL string, block string) []string {
|
|
seen := map[string]bool{}
|
|
images := []string{}
|
|
for _, pattern := range []string{`<img[^>]+src="([^"]+)"`, `!\[[^\]]*]\(([^)]+)\)`} {
|
|
for _, match := range regexp.MustCompile(pattern).FindAllStringSubmatch(block, -1) {
|
|
image := absoluteImage(baseURL, match[1])
|
|
if image != "" && !seen[image] {
|
|
seen[image] = true
|
|
images = append(images, image)
|
|
}
|
|
}
|
|
}
|
|
return images
|
|
}
|
|
|
|
func absoluteImage(baseURL, image string) string {
|
|
if image == "" || strings.HasPrefix(image, "http://") || strings.HasPrefix(image, "https://") {
|
|
return image
|
|
}
|
|
return baseURL + "/" + strings.TrimLeft(strings.TrimPrefix(image, "."), "/")
|
|
}
|
|
|
|
func leftPad(value int) string {
|
|
if value >= 1000 {
|
|
return strconv.Itoa(value)
|
|
}
|
|
text := "000" + strconv.Itoa(value)
|
|
return text[len(text)-3:]
|
|
}
|