feat: 引入 Eino 框架,实现四阶段生成管线
基于 compose.Graph 编排 PromptBuilder → AssetGenerator → QualitySupervisor → FormatAdapter, 含质检重试分支(最多 3 次)和降级输出。推理层提供 mock 模式,可替换为真实 API。
This commit is contained in:
+19
-1
@@ -2,13 +2,20 @@ module gen2d
|
||||
|
||||
go 1.26.3
|
||||
|
||||
require github.com/gin-gonic/gin v1.12.0
|
||||
require (
|
||||
github.com/cloudwego/eino v0.8.13
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/eino-contrib/jsonschema v1.0.3 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
@@ -16,22 +23,33 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/goph/emperror v0.17.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nikolalohinski/gonja v1.5.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yargevad/filepathx v1.0.0 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,20 +1,40 @@
|
||||
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
|
||||
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cloudwego/eino v0.8.13 h1:z5dhaZNN8TWZbP/lgKxGmF26Ii8fPeUlQCGV/NTtms0=
|
||||
github.com/cloudwego/eino v0.8.13/go.mod h1:+2N4nsMPxA6kGBHpH+75JuTfEcGprAMTdsZESrShKpU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
|
||||
github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -27,35 +47,85 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
|
||||
github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
|
||||
github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI=
|
||||
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg=
|
||||
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
|
||||
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
|
||||
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
|
||||
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
@@ -66,24 +136,46 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
|
||||
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// GenerateImages 调用 AI 推理 API 生成图片。
|
||||
// MVP 阶段返回 mock 占位图。
|
||||
func GenerateImages(ctx context.Context, prompt string, params AssetParams) ([]GeneratedImage, error) {
|
||||
size := params.Resolution
|
||||
if size <= 0 {
|
||||
size = 64
|
||||
}
|
||||
|
||||
count := 1
|
||||
if params.Frames.Directions > 0 && params.Frames.FramesPerDirection > 0 {
|
||||
count = params.Frames.Directions * params.Frames.FramesPerDirection
|
||||
}
|
||||
|
||||
images := make([]GeneratedImage, count)
|
||||
for i := 0; i < count; i++ {
|
||||
data, err := generateMockImage(size, i)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate mock image %d: %w", i, err)
|
||||
}
|
||||
images[i] = GeneratedImage{
|
||||
Data: data,
|
||||
Width: size,
|
||||
Height: size,
|
||||
Format: "png",
|
||||
}
|
||||
}
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// QualityChecker 质检函数,可替换用于测试。
|
||||
// 签名:(ctx, images, style) → (pass, reason, error)
|
||||
var QualityChecker = defaultCheckQuality
|
||||
|
||||
// CheckQuality 调用当前 QualityChecker。
|
||||
func CheckQuality(ctx context.Context, images []GeneratedImage, style map[string]string) (bool, string, error) {
|
||||
return QualityChecker(ctx, images, style)
|
||||
}
|
||||
|
||||
// defaultCheckQuality 默认 mock 质检,始终返回 pass。
|
||||
func defaultCheckQuality(ctx context.Context, images []GeneratedImage, style map[string]string) (bool, string, error) {
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
// NewCountedQualityChecker 创建一个在第 passOnRetry 次调用时返回 pass 的质检函数。
|
||||
func NewCountedQualityChecker(passOnRetry int) func(context.Context, []GeneratedImage, map[string]string) (bool, string, error) {
|
||||
var callCount int
|
||||
return func(_ context.Context, _ []GeneratedImage, _ map[string]string) (bool, string, error) {
|
||||
callCount++
|
||||
if callCount >= passOnRetry {
|
||||
return true, "", nil
|
||||
}
|
||||
return false, fmt.Sprintf("风格不一致(第 %d 次质检)", callCount), nil
|
||||
}
|
||||
}
|
||||
|
||||
// AlwaysFailQualityChecker 始终返回 fail 的质检函数。
|
||||
func AlwaysFailQualityChecker() func(context.Context, []GeneratedImage, map[string]string) (bool, string, error) {
|
||||
return func(_ context.Context, _ []GeneratedImage, _ map[string]string) (bool, string, error) {
|
||||
return false, "风格不一致", nil
|
||||
}
|
||||
}
|
||||
|
||||
// generateMockImage 生成一张带随机色块的 PNG 占位图
|
||||
func generateMockImage(size int, seed int) ([]byte, error) {
|
||||
img := image.NewRGBA(image.Rect(0, 0, size, size))
|
||||
|
||||
// 用 seed 生成不同颜色
|
||||
r := uint8((seed*47 + 13) % 256)
|
||||
g := uint8((seed*83 + 37) % 256)
|
||||
b := uint8((seed*61 + 71) % 256)
|
||||
|
||||
for y := 0; y < size; y++ {
|
||||
for x := 0; x < size; x++ {
|
||||
img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: 255})
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// generateRandomBytes 用于生成随机数据(备用)
|
||||
func generateRandomBytes(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
return b, err
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/compose"
|
||||
)
|
||||
|
||||
// PromptBuilder 节点:接收输入,输出三段式提示词
|
||||
var promptBuilderNode = compose.InvokableLambda(func(ctx context.Context, in PipelineInput) (string, error) {
|
||||
return buildPrompt(in), nil
|
||||
})
|
||||
|
||||
// promptBuilderPreHandler 首次运行时保存输入到 state;重试时注入 RejectReason
|
||||
func promptBuilderPreHandler(ctx context.Context, in PipelineInput, state *PipelineState) (PipelineInput, error) {
|
||||
if state.RetryCount == 0 {
|
||||
state.Input = in
|
||||
} else if state.RejectReason != "" {
|
||||
in.RejectReason = state.RejectReason
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
// promptBuilderPostHandler 将提示词写入全局状态
|
||||
func promptBuilderPostHandler(ctx context.Context, out string, state *PipelineState) (string, error) {
|
||||
state.FinalPrompt = out
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AssetGenerator 节点:调用 AI 推理 API 出图
|
||||
var assetGeneratorNode = compose.InvokableLambda(func(ctx context.Context, prompt string) ([]GeneratedImage, error) {
|
||||
var params AssetParams
|
||||
_ = compose.ProcessState[*PipelineState](ctx, func(_ context.Context, state *PipelineState) error {
|
||||
params = state.Input.Params
|
||||
return nil
|
||||
})
|
||||
return GenerateImages(ctx, prompt, params)
|
||||
})
|
||||
|
||||
// assetGeneratorPostHandler 将原始图片写入全局状态
|
||||
func assetGeneratorPostHandler(ctx context.Context, out []GeneratedImage, state *PipelineState) ([]GeneratedImage, error) {
|
||||
state.RawImages = out
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// QualitySupervisor 节点:质检,输出 PipelineInput 供下游节点消费。
|
||||
// 将图片存入 state,设置路由目标 NextNode。
|
||||
var qualitySupervisorNode = compose.InvokableLambda(func(ctx context.Context, images []GeneratedImage) (PipelineInput, error) {
|
||||
var input PipelineInput
|
||||
err := compose.ProcessState[*PipelineState](ctx, func(_ context.Context, state *PipelineState) error {
|
||||
// 保存图片到状态
|
||||
state.RawImages = images
|
||||
|
||||
// 质检
|
||||
style := mergeStyle(state.Input.ProjectStyle, state.Input.TaskStyle)
|
||||
pass, reason, checkErr := CheckQuality(ctx, images, style)
|
||||
if checkErr != nil {
|
||||
return fmt.Errorf("quality check: %w", checkErr)
|
||||
}
|
||||
|
||||
state.PassQuality = pass
|
||||
if !pass {
|
||||
state.RejectReason = reason
|
||||
}
|
||||
|
||||
// 决定路由
|
||||
if pass {
|
||||
state.NextNode = nodeFormatAdapter
|
||||
} else if state.RetryCount >= 3 {
|
||||
state.NextNode = nodeFormatAdapter // 超过重试次数,降级输出
|
||||
} else {
|
||||
state.RetryCount++
|
||||
state.NextNode = nodePromptBuilder // 重生成
|
||||
}
|
||||
|
||||
input = state.Input
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return PipelineInput{}, err
|
||||
}
|
||||
return input, nil
|
||||
})
|
||||
|
||||
// formatAdapterNode 节点:从 state 读取图片,格式转换,组装输出
|
||||
var formatAdapterNode = compose.InvokableLambda(func(ctx context.Context, input PipelineInput) (PipelineOutput, error) {
|
||||
var images []GeneratedImage
|
||||
_ = compose.ProcessState[*PipelineState](ctx, func(_ context.Context, state *PipelineState) error {
|
||||
images = state.RawImages
|
||||
return nil
|
||||
})
|
||||
|
||||
assets := make([]Asset, len(images))
|
||||
for i, img := range images {
|
||||
assets[i] = Asset{
|
||||
Data: img.Data,
|
||||
Format: img.Format,
|
||||
URL: fmt.Sprintf("output/%d.%s", i, img.Format),
|
||||
}
|
||||
}
|
||||
|
||||
resolution := input.Params.Resolution
|
||||
if resolution <= 0 {
|
||||
resolution = 64
|
||||
}
|
||||
|
||||
metadata := AssetMetadata{
|
||||
FrameWidth: resolution,
|
||||
FrameHeight: resolution,
|
||||
FrameCount: len(images),
|
||||
Directions: input.Params.Frames.Directions,
|
||||
}
|
||||
|
||||
return PipelineOutput{
|
||||
Assets: assets,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
})
|
||||
|
||||
// buildPrompt 构建三段式提示词
|
||||
func buildPrompt(in PipelineInput) string {
|
||||
var parts []string
|
||||
|
||||
// 【主题】
|
||||
parts = append(parts, fmt.Sprintf("【主题】%s", in.Prompt))
|
||||
|
||||
// 【约束】
|
||||
constraints := buildConstraints(in)
|
||||
parts = append(parts, fmt.Sprintf("【约束】%s", constraints))
|
||||
|
||||
// 【内容】
|
||||
content := buildContent(in)
|
||||
parts = append(parts, fmt.Sprintf("【内容】%s", content))
|
||||
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// buildConstraints 合并风格 + 负面提示词 + 重试原因
|
||||
func buildConstraints(in PipelineInput) string {
|
||||
style := mergeStyle(in.ProjectStyle, in.TaskStyle)
|
||||
|
||||
var parts []string
|
||||
for k, v := range style {
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", k, v))
|
||||
}
|
||||
|
||||
if in.RejectReason != "" {
|
||||
parts = append(parts, fmt.Sprintf("上次质检问题:%s", in.RejectReason))
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return "无特殊约束"
|
||||
}
|
||||
return strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
// buildContent 构建技术参数段
|
||||
func buildContent(in PipelineInput) string {
|
||||
var parts []string
|
||||
parts = append(parts, fmt.Sprintf("素材类型: %s", in.AssetType))
|
||||
if in.Params.Resolution > 0 {
|
||||
parts = append(parts, fmt.Sprintf("分辨率: %d", in.Params.Resolution))
|
||||
}
|
||||
if in.Params.Frames.Directions > 0 {
|
||||
parts = append(parts, fmt.Sprintf("方向数: %d", in.Params.Frames.Directions))
|
||||
}
|
||||
if in.Params.Frames.FramesPerDirection > 0 {
|
||||
parts = append(parts, fmt.Sprintf("每方向帧数: %d", in.Params.Frames.FramesPerDirection))
|
||||
}
|
||||
if in.Params.Format != "" {
|
||||
parts = append(parts, fmt.Sprintf("输出格式: %s", in.Params.Format))
|
||||
}
|
||||
return strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
// mergeStyle 合并工程风格与任务风格覆盖,任务同名键覆盖工程
|
||||
func mergeStyle(projectStyle, taskStyle map[string]string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
for k, v := range projectStyle {
|
||||
result[k] = v
|
||||
}
|
||||
for k, v := range taskStyle {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudwego/eino/compose"
|
||||
)
|
||||
|
||||
const (
|
||||
nodePromptBuilder = "prompt_builder"
|
||||
nodeAssetGenerator = "asset_generator"
|
||||
nodeQualitySupervisor = "quality_supervisor"
|
||||
nodeFormatAdapter = "format_adapter"
|
||||
)
|
||||
|
||||
// NewGenerateGraph 创建四阶段生成管线 Graph。
|
||||
//
|
||||
// START → PromptBuilder → AssetGenerator → QualitySupervisor
|
||||
// ├── pass → FormatAdapter → END
|
||||
// └── fail, retry<3 → PromptBuilder
|
||||
// └── fail, retry>=3 → FormatAdapter (降级)
|
||||
func NewGenerateGraph() (*compose.Graph[PipelineInput, PipelineOutput], error) {
|
||||
g := compose.NewGraph[PipelineInput, PipelineOutput](
|
||||
compose.WithGenLocalState(func(ctx context.Context) *PipelineState {
|
||||
return &PipelineState{}
|
||||
}),
|
||||
)
|
||||
|
||||
// 添加节点
|
||||
if err := g.AddLambdaNode(nodePromptBuilder, promptBuilderNode,
|
||||
compose.WithStatePreHandler(promptBuilderPreHandler),
|
||||
compose.WithStatePostHandler(promptBuilderPostHandler),
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("add %s node: %w", nodePromptBuilder, err)
|
||||
}
|
||||
|
||||
if err := g.AddLambdaNode(nodeAssetGenerator, assetGeneratorNode,
|
||||
compose.WithStatePostHandler(assetGeneratorPostHandler),
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("add %s node: %w", nodeAssetGenerator, err)
|
||||
}
|
||||
|
||||
if err := g.AddLambdaNode(nodeQualitySupervisor, qualitySupervisorNode); err != nil {
|
||||
return nil, fmt.Errorf("add %s node: %w", nodeQualitySupervisor, err)
|
||||
}
|
||||
|
||||
if err := g.AddLambdaNode(nodeFormatAdapter, formatAdapterNode); err != nil {
|
||||
return nil, fmt.Errorf("add %s node: %w", nodeFormatAdapter, err)
|
||||
}
|
||||
|
||||
// 连线:正常路径
|
||||
if err := g.AddEdge(compose.START, nodePromptBuilder); err != nil {
|
||||
return nil, fmt.Errorf("add edge START->%s: %w", nodePromptBuilder, err)
|
||||
}
|
||||
if err := g.AddEdge(nodePromptBuilder, nodeAssetGenerator); err != nil {
|
||||
return nil, fmt.Errorf("add edge %s->%s: %w", nodePromptBuilder, nodeAssetGenerator, err)
|
||||
}
|
||||
if err := g.AddEdge(nodeAssetGenerator, nodeQualitySupervisor); err != nil {
|
||||
return nil, fmt.Errorf("add edge %s->%s: %w", nodeAssetGenerator, nodeQualitySupervisor, err)
|
||||
}
|
||||
if err := g.AddEdge(nodeFormatAdapter, compose.END); err != nil {
|
||||
return nil, fmt.Errorf("add edge %s->END: %w", nodeFormatAdapter, err)
|
||||
}
|
||||
|
||||
// 连线:质检分支(从 state.NextNode 读取路由目标)
|
||||
if err := g.AddBranch(nodeQualitySupervisor, compose.NewGraphBranch(
|
||||
func(ctx context.Context, _ PipelineInput) (string, error) {
|
||||
var next string
|
||||
_ = compose.ProcessState[*PipelineState](ctx, func(_ context.Context, state *PipelineState) error {
|
||||
next = state.NextNode
|
||||
return nil
|
||||
})
|
||||
return next, nil
|
||||
},
|
||||
map[string]bool{nodePromptBuilder: true, nodeFormatAdapter: true},
|
||||
)); err != nil {
|
||||
return nil, fmt.Errorf("add branch at %s: %w", nodeQualitySupervisor, err)
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// RunPipeline 编译并执行生成管线
|
||||
func RunPipeline(ctx context.Context, in PipelineInput) (*PipelineOutput, error) {
|
||||
g, err := NewGenerateGraph()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create graph: %w", err)
|
||||
}
|
||||
|
||||
r, err := g.Compile(ctx, compose.WithMaxRunSteps(20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("compile graph: %w", err)
|
||||
}
|
||||
|
||||
output, err := r.Invoke(ctx, in)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invoke pipeline: %w", err)
|
||||
}
|
||||
|
||||
return &output, nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPipeline_HappyPath(t *testing.T) {
|
||||
// 质检一次通过
|
||||
QualityChecker = func(_ context.Context, _ []GeneratedImage, _ map[string]string) (bool, string, error) {
|
||||
return true, "", nil
|
||||
}
|
||||
defer func() { QualityChecker = defaultCheckQuality }()
|
||||
|
||||
output, err := RunPipeline(context.Background(), PipelineInput{
|
||||
Prompt: "一个拿剑的小人",
|
||||
AssetType: "sprite",
|
||||
ProjectStyle: map[string]string{
|
||||
"artStyle": "pixel",
|
||||
"palette": "warm",
|
||||
},
|
||||
Params: AssetParams{
|
||||
Resolution: 64,
|
||||
Format: "spritesheet",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunPipeline failed: %v", err)
|
||||
}
|
||||
|
||||
if len(output.Assets) == 0 {
|
||||
t.Fatal("expected non-empty assets")
|
||||
}
|
||||
if output.Metadata.FrameWidth != 64 {
|
||||
t.Errorf("expected FrameWidth=64, got %d", output.Metadata.FrameWidth)
|
||||
}
|
||||
if output.Metadata.FrameHeight != 64 {
|
||||
t.Errorf("expected FrameHeight=64, got %d", output.Metadata.FrameHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipeline_RetryThenPass(t *testing.T) {
|
||||
// 质检前 2 次 fail,第 3 次 pass
|
||||
QualityChecker = NewCountedQualityChecker(3)
|
||||
defer func() { QualityChecker = defaultCheckQuality }()
|
||||
|
||||
output, err := RunPipeline(context.Background(), PipelineInput{
|
||||
Prompt: "一把火焰剑",
|
||||
AssetType: "sprite",
|
||||
Params: AssetParams{
|
||||
Resolution: 32,
|
||||
Frames: FrameParams{
|
||||
Directions: 4,
|
||||
FramesPerDirection: 2,
|
||||
},
|
||||
Format: "spritesheet",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunPipeline failed: %v", err)
|
||||
}
|
||||
|
||||
// 4 directions × 2 frames = 8 张图
|
||||
if len(output.Assets) != 8 {
|
||||
t.Errorf("expected 8 assets, got %d", len(output.Assets))
|
||||
}
|
||||
if output.Metadata.FrameCount != 8 {
|
||||
t.Errorf("expected FrameCount=8, got %d", output.Metadata.FrameCount)
|
||||
}
|
||||
if output.Metadata.Directions != 4 {
|
||||
t.Errorf("expected Directions=4, got %d", output.Metadata.Directions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipeline_MaxRetryDegrade(t *testing.T) {
|
||||
// 质检始终 fail,超过 3 次后降级输出
|
||||
QualityChecker = AlwaysFailQualityChecker()
|
||||
defer func() { QualityChecker = defaultCheckQuality }()
|
||||
|
||||
output, err := RunPipeline(context.Background(), PipelineInput{
|
||||
Prompt: "一只飞龙",
|
||||
AssetType: "sprite",
|
||||
Params: AssetParams{
|
||||
Resolution: 48,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunPipeline failed: %v", err)
|
||||
}
|
||||
|
||||
// 降级也应该有输出
|
||||
if len(output.Assets) == 0 {
|
||||
t.Fatal("expected non-empty assets even on degrade")
|
||||
}
|
||||
if output.Metadata.FrameWidth != 48 {
|
||||
t.Errorf("expected FrameWidth=48, got %d", output.Metadata.FrameWidth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipeline_StyleMerge(t *testing.T) {
|
||||
// 验证风格合并:task 覆盖 project
|
||||
QualityChecker = func(_ context.Context, _ []GeneratedImage, style map[string]string) (bool, string, error) {
|
||||
// 验证合并结果
|
||||
if style["artStyle"] != "realistic" {
|
||||
t.Errorf("expected artStyle=realistic (task override), got %s", style["artStyle"])
|
||||
}
|
||||
if style["palette"] != "warm" {
|
||||
t.Errorf("expected palette=warm (from project), got %s", style["palette"])
|
||||
}
|
||||
return true, "", nil
|
||||
}
|
||||
defer func() { QualityChecker = defaultCheckQuality }()
|
||||
|
||||
_, err := RunPipeline(context.Background(), PipelineInput{
|
||||
Prompt: "测试风格合并",
|
||||
AssetType: "sprite",
|
||||
ProjectStyle: map[string]string{
|
||||
"artStyle": "pixel",
|
||||
"palette": "warm",
|
||||
},
|
||||
TaskStyle: map[string]string{
|
||||
"artStyle": "realistic", // 覆盖 project
|
||||
},
|
||||
Params: AssetParams{Resolution: 64},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunPipeline failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package service
|
||||
|
||||
// PipelineInput 管线入口输入
|
||||
type PipelineInput struct {
|
||||
Prompt string // 用户原始文本
|
||||
AssetType string // 素材类型:sprite / background / ui / animation
|
||||
ProjectStyle map[string]string // 工程风格键值对
|
||||
TaskStyle map[string]string // 任务风格覆盖
|
||||
Params AssetParams // 技术参数
|
||||
RejectReason string // 重试时由 state 注入
|
||||
}
|
||||
|
||||
// PipelineState Graph 全局状态,通过 WithGenLocalState 注入
|
||||
type PipelineState struct {
|
||||
Input PipelineInput
|
||||
FinalPrompt string // PromptBuilder 输出的三段式提示词
|
||||
RawImages []GeneratedImage // AssetGenerator 输出的原始图片
|
||||
PassQuality bool // QualitySupervisor 质检结果
|
||||
RejectReason string // 质检不通过原因
|
||||
RetryCount int // 重试次数
|
||||
NextNode string // QualitySupervisor 设置的路由目标
|
||||
}
|
||||
|
||||
// PipelineOutput 管线最终输出
|
||||
type PipelineOutput struct {
|
||||
Assets []Asset // 生成结果素材列表
|
||||
Metadata AssetMetadata // 元数据
|
||||
}
|
||||
|
||||
// AssetParams 技术参数
|
||||
type AssetParams struct {
|
||||
Resolution int
|
||||
Frames FrameParams
|
||||
Format string // "spritesheet" / "individual"
|
||||
}
|
||||
|
||||
// FrameParams 帧参数
|
||||
type FrameParams struct {
|
||||
Directions int
|
||||
FramesPerDirection int
|
||||
}
|
||||
|
||||
// GeneratedImage AI 生成的原始图片
|
||||
type GeneratedImage struct {
|
||||
Data []byte
|
||||
Width int
|
||||
Height int
|
||||
Format string
|
||||
}
|
||||
|
||||
// Asset 最终输出素材
|
||||
type Asset struct {
|
||||
Data []byte
|
||||
Format string
|
||||
URL string
|
||||
}
|
||||
|
||||
// AssetMetadata 素材元数据
|
||||
type AssetMetadata struct {
|
||||
FrameWidth int
|
||||
FrameHeight int
|
||||
FrameCount int
|
||||
Directions int
|
||||
}
|
||||
Reference in New Issue
Block a user