thread-master/backend/internal/library/viral/replicate.go

126 lines
4.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package viral
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"haixun-backend/internal/library/copyvoice"
"haixun-backend/internal/library/threadspost"
)
var codeFenceRE = regexp.MustCompile("(?s)```(?:json)?\\s*([\\s\\S]*?)```")
type ReplicateInput struct {
TopicLabel string
TopicBrief string
Persona string
StyleProfile string
OriginalText string
AuthorName string
StructureAnalysis string
NarrativeSeed string
}
type ReplicateResult struct {
Angle string `json:"angle"`
Hook string `json:"hook"`
Text string `json:"text"`
Rationale string `json:"rationale"`
StructureNotes string `json:"structureNotes"`
}
func BuildSystemPrompt() string {
return strings.TrimSpace(`你是 Threads / Instagram 創作者。讀完參考爆文後,用指定人設的語言指紋寫一篇全新貼文。
` + copyvoice.SystemRules() + `
輸出:
- text 主文 ≤ 500 字Threads API 硬上限,含 #話題標籤)
- 參考原文只學情緒力度、資訊密度與節奏;最後必須換成人設會講出口的話
- 篇幅、行數、標點與換行優先遵守人設語言指紋;沒有指紋時才貼近參考原文
- text 排版要像真人手機發文:可使用自然標點、空行、列點與少量 emoji不要完全無標點也不要每句硬換行除非人設樣本就是這樣
- 只回傳一個 JSON 物件欄位angle, hook, text, rationale, structureNotes`)
}
func BuildUserPrompt(input ReplicateInput) string {
var b strings.Builder
b.WriteString("主題:")
b.WriteString(strings.TrimSpace(input.TopicLabel))
b.WriteString("\n")
if brief := strings.TrimSpace(input.TopicBrief); brief != "" {
b.WriteString("Brief")
b.WriteString(brief)
b.WriteString("\n")
}
if persona := strings.TrimSpace(input.Persona); persona != "" {
b.WriteString("\n人設定位、語言指紋與禁忌最高優先\n")
b.WriteString(persona)
b.WriteString("\n")
b.WriteString(copyvoice.PersonaCalibrationNote())
b.WriteString("\n")
}
if style := strings.TrimSpace(input.StyleProfile); style != "" {
b.WriteString("\n8D 風格策略:\n")
b.WriteString(style)
b.WriteString("\n")
}
if analysis := strings.TrimSpace(input.StructureAnalysis); analysis != "" {
b.WriteString("\n敘事分析只學故事感不要套句型或段落公式\n")
b.WriteString(analysis)
b.WriteString("\n")
}
author := strings.TrimSpace(input.AuthorName)
if author == "" {
author = "匿名"
}
original := strings.TrimSpace(input.OriginalText)
if note := threadspost.MimicLengthGuidance(original); note != "" {
b.WriteString("\n")
b.WriteString(note)
b.WriteString("\n")
}
b.WriteString("\n原文參考@")
b.WriteString(author)
b.WriteString(",只感受情緒、敘事與篇幅,禁止抄句型):\n")
b.WriteString(original)
b.WriteString("\n\n本次敘事切入")
b.WriteString(copyvoice.NarrativeLens(input.NarrativeSeed))
b.WriteString("\n\n請產出一篇可發布的全新貼文 JSON。hook 欄位只放內部分析,不要把模板句型寫進 text。")
return b.String()
}
func ParseReplicateOutput(raw string) (ReplicateResult, error) {
payload, err := extractJSONObject(raw)
if err != nil {
return ReplicateResult{}, err
}
var out ReplicateResult
if err := json.Unmarshal(payload, &out); err != nil {
return ReplicateResult{}, fmt.Errorf("parse viral replica json: %w", err)
}
out.Text = threadspost.FormatDraftText(out.Text)
if out.Text == "" {
return ReplicateResult{}, fmt.Errorf("replica text missing")
}
out.Angle = strings.TrimSpace(out.Angle)
out.Hook = strings.TrimSpace(out.Hook)
out.Rationale = strings.TrimSpace(out.Rationale)
out.StructureNotes = strings.TrimSpace(out.StructureNotes)
return out, nil
}
func extractJSONObject(raw string) ([]byte, error) {
raw = strings.TrimSpace(raw)
if m := codeFenceRE.FindStringSubmatch(raw); len(m) == 2 {
raw = strings.TrimSpace(m[1])
}
start := strings.Index(raw, "{")
end := strings.LastIndex(raw, "}")
if start < 0 || end <= start {
return nil, fmt.Errorf("replica output missing json object")
}
return []byte(raw[start : end+1]), nil
}