Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
a00c6393
authored
Oct 28, 2025
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement audio duration retrieval without ffmpeg dependencies
parent
a25c11df
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
387 additions
and
136 deletions
+387
-136
Dockerfile
+1
-1
common/audio.go
+295
-0
common/gin.go
+4
-4
common/utils.go
+0
-39
go.mod
+13
-0
go.sum
+0
-0
middleware/distributor.go
+44
-27
relay/channel/openai/relay-openai.go
+1
-53
relay/helper/valid_request.go
+0
-10
service/token_counter.go
+29
-2
No files found.
Dockerfile
View file @
a00c6393
...
...
@@ -28,7 +28,7 @@ RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$
FROM
alpine
RUN
apk upgrade
--no-cache
\
&&
apk add
--no-cache
ca-certificates tzdata
ffmpeg
\
&&
apk add
--no-cache
ca-certificates tzdata
\
&&
update-ca-certificates
COPY
--from=builder2 /build/new-api /
...
...
common/audio.go
0 → 100644
View file @
a00c6393
package
common
import
(
"context"
"encoding/binary"
"fmt"
"io"
"github.com/abema/go-mp4"
"github.com/go-audio/aiff"
"github.com/go-audio/wav"
"github.com/jfreymuth/oggvorbis"
"github.com/mewkiz/flac"
"github.com/pkg/errors"
"github.com/tcolgate/mp3"
"github.com/yapingcat/gomedia/go-codec"
)
// GetAudioDuration 使用纯 Go 库获取音频文件的时长(秒)。
// 它不再依赖外部的 ffmpeg 或 ffprobe 程序。
func
GetAudioDuration
(
ctx
context
.
Context
,
f
io
.
ReadSeeker
,
ext
string
)
(
duration
float64
,
err
error
)
{
SysLog
(
fmt
.
Sprintf
(
"GetAudioDuration: ext=%s"
,
ext
))
// 根据文件扩展名选择解析器
switch
ext
{
case
".mp3"
:
duration
,
err
=
getMP3Duration
(
f
)
case
".wav"
:
duration
,
err
=
getWAVDuration
(
f
)
case
".flac"
:
duration
,
err
=
getFLACDuration
(
f
)
case
".m4a"
,
".mp4"
:
duration
,
err
=
getM4ADuration
(
f
)
case
".ogg"
,
".oga"
:
duration
,
err
=
getOGGDuration
(
f
)
case
".opus"
:
duration
,
err
=
getOpusDuration
(
f
)
case
".aiff"
,
".aif"
,
".aifc"
:
duration
,
err
=
getAIFFDuration
(
f
)
case
".webm"
:
duration
,
err
=
getWebMDuration
(
f
)
case
".aac"
:
duration
,
err
=
getAACDuration
(
f
)
default
:
return
0
,
fmt
.
Errorf
(
"unsupported audio format: %s"
,
ext
)
}
SysLog
(
fmt
.
Sprintf
(
"GetAudioDuration: duration=%f"
,
duration
))
return
duration
,
err
}
// getMP3Duration 解析 MP3 文件以获取时长。
// 注意:对于 VBR (Variable Bitrate) MP3,这个估算可能不完全精确,但通常足够好。
// FFmpeg 在这种情况下会扫描整个文件来获得精确值,但这里的库提供了快速估算。
func
getMP3Duration
(
r
io
.
Reader
)
(
float64
,
error
)
{
d
:=
mp3
.
NewDecoder
(
r
)
var
f
mp3
.
Frame
skipped
:=
0
duration
:=
0.0
for
{
if
err
:=
d
.
Decode
(
&
f
,
&
skipped
);
err
!=
nil
{
if
err
==
io
.
EOF
{
break
}
return
0
,
errors
.
Wrap
(
err
,
"failed to decode mp3 frame"
)
}
duration
+=
f
.
Duration
()
.
Seconds
()
}
return
duration
,
nil
}
// getWAVDuration 解析 WAV 文件头以获取时长。
func
getWAVDuration
(
r
io
.
ReadSeeker
)
(
float64
,
error
)
{
dec
:=
wav
.
NewDecoder
(
r
)
if
!
dec
.
IsValidFile
()
{
return
0
,
errors
.
New
(
"invalid wav file"
)
}
d
,
err
:=
dec
.
Duration
()
if
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to get wav duration"
)
}
return
d
.
Seconds
(),
nil
}
// getFLACDuration 解析 FLAC 文件的 STREAMINFO 块。
func
getFLACDuration
(
r
io
.
Reader
)
(
float64
,
error
)
{
stream
,
err
:=
flac
.
Parse
(
r
)
if
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to parse flac stream"
)
}
defer
stream
.
Close
()
// 时长 = 总采样数 / 采样率
duration
:=
float64
(
stream
.
Info
.
NSamples
)
/
float64
(
stream
.
Info
.
SampleRate
)
return
duration
,
nil
}
// getM4ADuration 解析 M4A/MP4 文件的 'mvhd' box。
func
getM4ADuration
(
r
io
.
ReadSeeker
)
(
float64
,
error
)
{
// go-mp4 库需要 ReadSeeker 接口
info
,
err
:=
mp4
.
Probe
(
r
)
if
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to probe m4a/mp4 file"
)
}
// 时长 = Duration / Timescale
return
float64
(
info
.
Duration
)
/
float64
(
info
.
Timescale
),
nil
}
// getOGGDuration 解析 OGG/Vorbis 文件以获取时长。
func
getOGGDuration
(
r
io
.
ReadSeeker
)
(
float64
,
error
)
{
// 重置 reader 到开头
if
_
,
err
:=
r
.
Seek
(
0
,
io
.
SeekStart
);
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to seek ogg file"
)
}
reader
,
err
:=
oggvorbis
.
NewReader
(
r
)
if
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to create ogg vorbis reader"
)
}
// 计算时长 = 总采样数 / 采样率
// 需要读取整个文件来获取总采样数
channels
:=
reader
.
Channels
()
sampleRate
:=
reader
.
SampleRate
()
// 估算方法:读取到文件结尾
var
totalSamples
int64
buf
:=
make
([]
float32
,
4096
*
channels
)
for
{
n
,
err
:=
reader
.
Read
(
buf
)
if
err
==
io
.
EOF
{
break
}
if
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to read ogg samples"
)
}
totalSamples
+=
int64
(
n
/
channels
)
}
duration
:=
float64
(
totalSamples
)
/
float64
(
sampleRate
)
return
duration
,
nil
}
// getOpusDuration 解析 Opus 文件(在 OGG 容器中)以获取时长。
func
getOpusDuration
(
r
io
.
ReadSeeker
)
(
float64
,
error
)
{
// Opus 通常封装在 OGG 容器中
// 我们需要解析 OGG 页面来获取时长信息
if
_
,
err
:=
r
.
Seek
(
0
,
io
.
SeekStart
);
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to seek opus file"
)
}
// 读取 OGG 页面头部
var
totalGranulePos
int64
buf
:=
make
([]
byte
,
27
)
// OGG 页面头部最小大小
for
{
n
,
err
:=
r
.
Read
(
buf
)
if
err
==
io
.
EOF
{
break
}
if
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to read opus/ogg page"
)
}
if
n
<
27
{
break
}
// 检查 OGG 页面标识 "OggS"
if
string
(
buf
[
0
:
4
])
!=
"OggS"
{
// 跳过一些字节继续寻找
if
_
,
err
:=
r
.
Seek
(
-
26
,
io
.
SeekCurrent
);
err
!=
nil
{
break
}
continue
}
// 读取 granule position (字节 6-13, 小端序)
granulePos
:=
int64
(
binary
.
LittleEndian
.
Uint64
(
buf
[
6
:
14
]))
if
granulePos
>
totalGranulePos
{
totalGranulePos
=
granulePos
}
// 读取段表大小
numSegments
:=
int
(
buf
[
26
])
segmentTable
:=
make
([]
byte
,
numSegments
)
if
_
,
err
:=
io
.
ReadFull
(
r
,
segmentTable
);
err
!=
nil
{
break
}
// 计算页面数据大小并跳过
var
pageSize
int
for
_
,
segSize
:=
range
segmentTable
{
pageSize
+=
int
(
segSize
)
}
if
_
,
err
:=
r
.
Seek
(
int64
(
pageSize
),
io
.
SeekCurrent
);
err
!=
nil
{
break
}
}
// Opus 的采样率固定为 48000 Hz
duration
:=
float64
(
totalGranulePos
)
/
48000.0
return
duration
,
nil
}
// getAIFFDuration 解析 AIFF 文件头以获取时长。
func
getAIFFDuration
(
r
io
.
ReadSeeker
)
(
float64
,
error
)
{
if
_
,
err
:=
r
.
Seek
(
0
,
io
.
SeekStart
);
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to seek aiff file"
)
}
dec
:=
aiff
.
NewDecoder
(
r
)
if
!
dec
.
IsValidFile
()
{
return
0
,
errors
.
New
(
"invalid aiff file"
)
}
d
,
err
:=
dec
.
Duration
()
if
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to get aiff duration"
)
}
return
d
.
Seconds
(),
nil
}
// getWebMDuration 解析 WebM 文件以获取时长。
// WebM 使用 Matroska 容器格式
func
getWebMDuration
(
r
io
.
ReadSeeker
)
(
float64
,
error
)
{
if
_
,
err
:=
r
.
Seek
(
0
,
io
.
SeekStart
);
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to seek webm file"
)
}
// WebM/Matroska 文件的解析比较复杂
// 这里提供一个简化的实现,读取 EBML 头部
// 对于完整的 WebM 解析,可能需要使用专门的库
// 简单实现:查找 Duration 元素
// WebM Duration 的 Element ID 是 0x4489
// 这是一个简化版本,可能不适用于所有 WebM 文件
buf
:=
make
([]
byte
,
8192
)
n
,
err
:=
r
.
Read
(
buf
)
if
err
!=
nil
&&
err
!=
io
.
EOF
{
return
0
,
errors
.
Wrap
(
err
,
"failed to read webm file"
)
}
// 尝试查找 Duration 元素(这是一个简化的方法)
// 实际的 WebM 解析需要完整的 EBML 解析器
// 这里返回错误,建议使用专门的库
if
n
>
0
{
// 检查 EBML 标识
if
len
(
buf
)
>=
4
&&
binary
.
BigEndian
.
Uint32
(
buf
[
0
:
4
])
==
0x1A45DFA3
{
// 这是一个有效的 EBML 文件
// 但完整解析需要更复杂的逻辑
return
0
,
errors
.
New
(
"webm duration parsing requires full EBML parser (consider using ffprobe for webm files)"
)
}
}
return
0
,
errors
.
New
(
"failed to parse webm file"
)
}
// getAACDuration 解析 AAC (ADTS格式) 文件以获取时长。
// 使用 gomedia 库来解析 AAC ADTS 帧
func
getAACDuration
(
r
io
.
ReadSeeker
)
(
float64
,
error
)
{
if
_
,
err
:=
r
.
Seek
(
0
,
io
.
SeekStart
);
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to seek aac file"
)
}
// 读取整个文件内容
data
,
err
:=
io
.
ReadAll
(
r
)
if
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to read aac file"
)
}
var
totalFrames
int64
var
sampleRate
int
// 使用 gomedia 的 SplitAACFrame 函数来分割 AAC 帧
codec
.
SplitAACFrame
(
data
,
func
(
aac
[]
byte
)
{
// 解析 ADTS 头部以获取采样率信息
if
len
(
aac
)
>=
7
{
// 使用 ConvertADTSToASC 来获取音频配置信息
asc
,
err
:=
codec
.
ConvertADTSToASC
(
aac
)
if
err
==
nil
&&
sampleRate
==
0
{
sampleRate
=
codec
.
AACSampleIdxToSample
(
int
(
asc
.
Sample_freq_index
))
}
totalFrames
++
}
})
if
sampleRate
==
0
||
totalFrames
==
0
{
return
0
,
errors
.
New
(
"no valid aac frames found"
)
}
// 每个 AAC ADTS 帧包含 1024 个采样
totalSamples
:=
totalFrames
*
1024
duration
:=
float64
(
totalSamples
)
/
float64
(
sampleRate
)
return
duration
,
nil
}
common/gin.go
View file @
a00c6393
...
...
@@ -163,7 +163,7 @@ func parseFormData(data []byte, v any) error {
return
err
}
return
json
.
Unmarshal
(
jsonData
,
v
)
return
Unmarshal
(
jsonData
,
v
)
}
func
parseMultipartFormData
(
c
*
gin
.
Context
,
data
[]
byte
,
v
any
)
error
{
...
...
@@ -174,7 +174,7 @@ func parseMultipartFormData(c *gin.Context, data []byte, v any) error {
}
if
boundary
==
""
{
return
json
.
Unmarshal
(
data
,
v
)
// Fallback to JSON
return
Unmarshal
(
data
,
v
)
// Fallback to JSON
}
reader
:=
multipart
.
NewReader
(
bytes
.
NewReader
(
data
),
boundary
)
...
...
@@ -191,10 +191,10 @@ func parseMultipartFormData(c *gin.Context, data []byte, v any) error {
formMap
[
key
]
=
vals
}
}
jsonData
,
err
:=
json
.
Marshal
(
formMap
)
jsonData
,
err
:=
Marshal
(
formMap
)
if
err
!=
nil
{
return
err
}
return
json
.
Unmarshal
(
jsonData
,
v
)
return
Unmarshal
(
jsonData
,
v
)
}
common/utils.go
View file @
a00c6393
package
common
import
(
"bytes"
"context"
crand
"crypto/rand"
"encoding/base64"
"encoding/json"
...
...
@@ -329,43 +327,6 @@ func SaveTmpFile(filename string, data io.Reader) (string, error) {
return
f
.
Name
(),
nil
}
// GetAudioDuration returns the duration of an audio file in seconds.
func
GetAudioDuration
(
ctx
context
.
Context
,
filename
string
,
ext
string
)
(
float64
,
error
)
{
// ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {{input}}
c
:=
exec
.
CommandContext
(
ctx
,
"ffprobe"
,
"-v"
,
"error"
,
"-show_entries"
,
"format=duration"
,
"-of"
,
"default=noprint_wrappers=1:nokey=1"
,
filename
)
output
,
err
:=
c
.
Output
()
if
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to get audio duration"
)
}
durationStr
:=
string
(
bytes
.
TrimSpace
(
output
))
if
durationStr
==
"N/A"
{
// Create a temporary output file name
tmpFp
,
err
:=
os
.
CreateTemp
(
""
,
"audio-*"
+
ext
)
if
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to create temporary file"
)
}
tmpName
:=
tmpFp
.
Name
()
// Close immediately so ffmpeg can open the file on Windows.
_
=
tmpFp
.
Close
()
defer
os
.
Remove
(
tmpName
)
// ffmpeg -y -i filename -vcodec copy -acodec copy <tmpName>
ffmpegCmd
:=
exec
.
CommandContext
(
ctx
,
"ffmpeg"
,
"-y"
,
"-i"
,
filename
,
"-vcodec"
,
"copy"
,
"-acodec"
,
"copy"
,
tmpName
)
if
err
:=
ffmpegCmd
.
Run
();
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to run ffmpeg"
)
}
// Recalculate the duration of the new file
c
=
exec
.
CommandContext
(
ctx
,
"ffprobe"
,
"-v"
,
"error"
,
"-show_entries"
,
"format=duration"
,
"-of"
,
"default=noprint_wrappers=1:nokey=1"
,
tmpName
)
output
,
err
:=
c
.
Output
()
if
err
!=
nil
{
return
0
,
errors
.
Wrap
(
err
,
"failed to get audio duration after ffmpeg"
)
}
durationStr
=
string
(
bytes
.
TrimSpace
(
output
))
}
return
strconv
.
ParseFloat
(
durationStr
,
64
)
}
// BuildURL concatenates base and endpoint, returns the complete url string
func
BuildURL
(
base
string
,
endpoint
string
)
string
{
u
,
err
:=
url
.
Parse
(
base
)
...
...
go.mod
View file @
a00c6393
...
...
@@ -5,6 +5,7 @@ go 1.25.1
require (
github.com/Calcium-Ion/go-epay v0.0.4
github.com/abema/go-mp4 v1.4.1
github.com/andybalholm/brotli v1.1.1
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0
github.com/aws/aws-sdk-go-v2 v1.37.2
...
...
@@ -18,24 +19,30 @@ require (
github.com/gin-contrib/static v0.0.1
github.com/gin-gonic/gin v1.9.1
github.com/glebarez/sqlite v1.9.0
github.com/go-audio/aiff v1.1.0
github.com/go-audio/wav v1.1.0
github.com/go-playground/validator/v10 v10.20.0
github.com/go-redis/redis/v8 v8.11.5
github.com/go-webauthn/webauthn v0.14.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.0
github.com/jfreymuth/oggvorbis v1.0.5
github.com/jinzhu/copier v0.4.0
github.com/joho/godotenv v1.5.1
github.com/mewkiz/flac v1.0.13
github.com/pkg/errors v0.9.1
github.com/pquerna/otp v1.5.0
github.com/samber/lo v1.39.0
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/shopspring/decimal v1.4.0
github.com/stripe/stripe-go/v81 v81.4.0
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300
github.com/thanhpk/randstr v1.0.6
github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5
github.com/tiktoken-go/tokenizer v0.6.2
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c
golang.org/x/crypto v0.42.0
golang.org/x/image v0.23.0
golang.org/x/net v0.43.0
...
...
@@ -62,6 +69,8 @@ require (
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-audio/audio v1.0.0 // indirect
github.com/go-audio/riff v1.0.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
...
...
@@ -73,16 +82,20 @@ require (
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/icza/bitio v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jfreymuth/vorbis v1.0.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // 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/mattn/go-isatty v0.0.20 // indirect
github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d // indirect
github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
...
...
go.sum
View file @
a00c6393
This diff is collapsed.
Click to expand it.
middleware/distributor.go
View file @
a00c6393
...
...
@@ -86,7 +86,7 @@ func Distribute() func(c *gin.Context) {
playgroundRequest
:=
&
dto
.
PlayGroundRequest
{}
err
=
common
.
UnmarshalBodyReusable
(
c
,
playgroundRequest
)
if
err
!=
nil
{
abortWithOpenAiMessage
(
c
,
http
.
StatusBadRequest
,
"无效的请求, "
+
err
.
Error
())
abortWithOpenAiMessage
(
c
,
http
.
StatusBadRequest
,
"无效的
playground
请求, "
+
err
.
Error
())
return
}
if
playgroundRequest
.
Group
!=
""
{
...
...
@@ -124,6 +124,20 @@ func Distribute() func(c *gin.Context) {
}
}
// getModelFromRequest 从请求中读取模型信息
// 根据 Content-Type 自动处理:
// - application/json
// - application/x-www-form-urlencoded
// - multipart/form-data
func
getModelFromRequest
(
c
*
gin
.
Context
)
(
*
ModelRequest
,
error
)
{
var
modelRequest
ModelRequest
err
:=
common
.
UnmarshalBodyReusable
(
c
,
&
modelRequest
)
if
err
!=
nil
{
return
nil
,
errors
.
New
(
"无效的请求, "
+
err
.
Error
())
}
return
&
modelRequest
,
nil
}
func
getModelRequest
(
c
*
gin
.
Context
)
(
*
ModelRequest
,
bool
,
error
)
{
var
modelRequest
ModelRequest
shouldSelectChannel
:=
true
...
...
@@ -139,7 +153,7 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
midjourneyRequest
:=
dto
.
MidjourneyRequest
{}
err
=
common
.
UnmarshalBodyReusable
(
c
,
&
midjourneyRequest
)
if
err
!=
nil
{
return
nil
,
false
,
err
return
nil
,
false
,
err
ors
.
New
(
"无效的midjourney请求, "
+
err
.
Error
())
}
midjourneyModel
,
mjErr
,
success
:=
service
.
GetMjRequestModel
(
relayMode
,
&
midjourneyRequest
)
if
mjErr
!=
nil
{
...
...
@@ -176,23 +190,12 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
relayMode
:=
relayconstant
.
RelayModeUnknown
if
c
.
Request
.
Method
==
http
.
MethodPost
{
relayMode
=
relayconstant
.
RelayModeVideoSubmit
contentType
:=
c
.
Request
.
Header
.
Get
(
"Content-Type"
)
if
strings
.
HasPrefix
(
contentType
,
"multipart/form-data"
)
{
form
,
err
:=
common
.
ParseMultipartFormReusable
(
c
)
if
err
!=
nil
{
return
nil
,
false
,
errors
.
New
(
"无效的video请求, "
+
err
.
Error
())
}
defer
form
.
RemoveAll
()
if
form
!=
nil
{
if
values
,
ok
:=
form
.
Value
[
"model"
];
ok
&&
len
(
values
)
>
0
{
modelRequest
.
Model
=
values
[
0
]
}
}
}
else
if
strings
.
HasPrefix
(
contentType
,
"application/json"
)
{
err
=
common
.
UnmarshalBodyReusable
(
c
,
&
modelRequest
)
req
,
err
:=
getModelFromRequest
(
c
)
if
err
!=
nil
{
return
nil
,
false
,
errors
.
New
(
"无效的video请求, "
+
err
.
Error
())
return
nil
,
false
,
err
}
if
req
!=
nil
{
modelRequest
.
Model
=
req
.
Model
}
}
else
if
c
.
Request
.
Method
==
http
.
MethodGet
{
relayMode
=
relayconstant
.
RelayModeVideoFetchByID
...
...
@@ -202,10 +205,11 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
}
else
if
strings
.
Contains
(
c
.
Request
.
URL
.
Path
,
"/v1/video/generations"
)
{
relayMode
:=
relayconstant
.
RelayModeUnknown
if
c
.
Request
.
Method
==
http
.
MethodPost
{
err
=
common
.
UnmarshalBodyReusable
(
c
,
&
modelRequest
)
req
,
err
:=
getModelFromRequest
(
c
)
if
err
!=
nil
{
return
nil
,
false
,
err
ors
.
New
(
"video无效的请求, "
+
err
.
Error
())
return
nil
,
false
,
err
}
modelRequest
.
Model
=
req
.
Model
relayMode
=
relayconstant
.
RelayModeVideoSubmit
}
else
if
c
.
Request
.
Method
==
http
.
MethodGet
{
relayMode
=
relayconstant
.
RelayModeVideoFetchByID
...
...
@@ -223,10 +227,11 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
}
c
.
Set
(
"relay_mode"
,
relayMode
)
}
else
if
!
strings
.
HasPrefix
(
c
.
Request
.
URL
.
Path
,
"/v1/audio/transcriptions"
)
&&
!
strings
.
Contains
(
c
.
Request
.
Header
.
Get
(
"Content-Type"
),
"multipart/form-data"
)
{
err
=
common
.
UnmarshalBodyReusable
(
c
,
&
modelRequest
)
}
req
,
err
:=
getModelFromRequest
(
c
)
if
err
!=
nil
{
return
nil
,
false
,
errors
.
New
(
"无效的请求, "
+
err
.
Error
())
return
nil
,
false
,
err
}
modelRequest
.
Model
=
req
.
Model
}
if
strings
.
HasPrefix
(
c
.
Request
.
URL
.
Path
,
"/v1/realtime"
)
{
//wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01
...
...
@@ -248,19 +253,29 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
//modelRequest.Model = common.GetStringIfEmpty(c.PostForm("model"), "gpt-image-1")
contentType
:=
c
.
ContentType
()
if
slices
.
Contains
([]
string
{
gin
.
MIMEPOSTForm
,
gin
.
MIMEMultipartPOSTForm
},
contentType
)
{
modelRequest
.
Model
=
c
.
PostForm
(
"model"
)
req
,
err
:=
getModelFromRequest
(
c
)
if
err
==
nil
&&
req
.
Model
!=
""
{
modelRequest
.
Model
=
req
.
Model
}
}
}
if
strings
.
HasPrefix
(
c
.
Request
.
URL
.
Path
,
"/v1/audio"
)
{
relayMode
:=
relayconstant
.
RelayModeAudioSpeech
if
strings
.
HasPrefix
(
c
.
Request
.
URL
.
Path
,
"/v1/audio/speech"
)
{
modelRequest
.
Model
=
common
.
GetStringIfEmpty
(
modelRequest
.
Model
,
"tts-1"
)
}
else
if
strings
.
HasPrefix
(
c
.
Request
.
URL
.
Path
,
"/v1/audio/translations"
)
{
modelRequest
.
Model
=
common
.
GetStringIfEmpty
(
modelRequest
.
Model
,
c
.
PostForm
(
"model"
))
// 先尝试从请求读取
if
req
,
err
:=
getModelFromRequest
(
c
);
err
==
nil
&&
req
.
Model
!=
""
{
modelRequest
.
Model
=
req
.
Model
}
modelRequest
.
Model
=
common
.
GetStringIfEmpty
(
modelRequest
.
Model
,
"whisper-1"
)
relayMode
=
relayconstant
.
RelayModeAudioTranslation
}
else
if
strings
.
HasPrefix
(
c
.
Request
.
URL
.
Path
,
"/v1/audio/transcriptions"
)
{
modelRequest
.
Model
=
common
.
GetStringIfEmpty
(
modelRequest
.
Model
,
c
.
PostForm
(
"model"
))
// 先尝试从请求读取
if
req
,
err
:=
getModelFromRequest
(
c
);
err
==
nil
&&
req
.
Model
!=
""
{
modelRequest
.
Model
=
req
.
Model
}
modelRequest
.
Model
=
common
.
GetStringIfEmpty
(
modelRequest
.
Model
,
"whisper-1"
)
relayMode
=
relayconstant
.
RelayModeAudioTranscription
}
...
...
@@ -268,10 +283,12 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
}
if
strings
.
HasPrefix
(
c
.
Request
.
URL
.
Path
,
"/pg/chat/completions"
)
{
// playground chat completions
err
=
common
.
UnmarshalBodyReusable
(
c
,
&
modelRequest
)
req
,
err
:=
getModelFromRequest
(
c
)
if
err
!=
nil
{
return
nil
,
false
,
err
ors
.
New
(
"无效的请求, "
+
err
.
Error
())
return
nil
,
false
,
err
}
modelRequest
.
Model
=
req
.
Model
modelRequest
.
Group
=
req
.
Group
common
.
SetContextKey
(
c
,
constant
.
ContextKeyTokenGroup
,
modelRequest
.
Group
)
}
return
&
modelRequest
,
shouldSelectChannel
,
nil
...
...
relay/channel/openai/relay-openai.go
View file @
a00c6393
package
openai
import
(
"bytes"
"encoding/json"
"fmt"
"io"
"math"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/QuantumNous/new-api/common"
...
...
@@ -26,7 +21,6 @@ import (
"github.com/bytedance/gopkg/util/gopool"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)
func
sendStreamData
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
data
string
,
forceFormat
bool
,
thinkToContent
bool
)
error
{
...
...
@@ -361,59 +355,13 @@ func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
}
}
audioTokens
,
err
:=
countAudioTokens
(
c
)
if
err
!=
nil
{
return
types
.
NewError
(
err
,
types
.
ErrorCodeCountTokenFailed
),
nil
}
usage
:=
&
dto
.
Usage
{}
usage
.
PromptTokens
=
audio
Tokens
usage
.
PromptTokens
=
info
.
Prompt
Tokens
usage
.
CompletionTokens
=
0
usage
.
TotalTokens
=
usage
.
PromptTokens
+
usage
.
CompletionTokens
return
nil
,
usage
}
func
countAudioTokens
(
c
*
gin
.
Context
)
(
int
,
error
)
{
body
,
err
:=
common
.
GetRequestBody
(
c
)
if
err
!=
nil
{
return
0
,
errors
.
WithStack
(
err
)
}
var
reqBody
struct
{
File
*
multipart
.
FileHeader
`form:"file" binding:"required"`
}
c
.
Request
.
Body
=
io
.
NopCloser
(
bytes
.
NewReader
(
body
))
if
err
=
c
.
ShouldBind
(
&
reqBody
);
err
!=
nil
{
return
0
,
errors
.
WithStack
(
err
)
}
ext
:=
filepath
.
Ext
(
reqBody
.
File
.
Filename
)
// 获取文件扩展名
reqFp
,
err
:=
reqBody
.
File
.
Open
()
if
err
!=
nil
{
return
0
,
errors
.
WithStack
(
err
)
}
defer
reqFp
.
Close
()
tmpFp
,
err
:=
os
.
CreateTemp
(
""
,
"audio-*"
+
ext
)
if
err
!=
nil
{
return
0
,
errors
.
WithStack
(
err
)
}
defer
os
.
Remove
(
tmpFp
.
Name
())
_
,
err
=
io
.
Copy
(
tmpFp
,
reqFp
)
if
err
!=
nil
{
return
0
,
errors
.
WithStack
(
err
)
}
if
err
=
tmpFp
.
Close
();
err
!=
nil
{
return
0
,
errors
.
WithStack
(
err
)
}
duration
,
err
:=
common
.
GetAudioDuration
(
c
.
Request
.
Context
(),
tmpFp
.
Name
(),
ext
)
if
err
!=
nil
{
return
0
,
errors
.
WithStack
(
err
)
}
return
int
(
math
.
Round
(
math
.
Ceil
(
duration
)
/
60.0
*
1000
)),
nil
// 1 minute 相当于 1k tokens
}
func
OpenaiRealtimeHandler
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
)
(
*
types
.
NewAPIError
,
*
dto
.
RealtimeUsage
)
{
if
info
==
nil
||
info
.
ClientWs
==
nil
||
info
.
TargetWs
==
nil
{
return
types
.
NewError
(
fmt
.
Errorf
(
"invalid websocket connection"
),
types
.
ErrorCodeBadResponse
),
nil
...
...
relay/helper/valid_request.go
View file @
a00c6393
...
...
@@ -62,19 +62,9 @@ func GetAndValidAudioRequest(c *gin.Context, relayMode int) (*dto.AudioRequest,
return
nil
,
errors
.
New
(
"model is required"
)
}
default
:
err
=
c
.
Request
.
ParseForm
()
if
err
!=
nil
{
return
nil
,
err
}
formData
:=
c
.
Request
.
PostForm
if
audioRequest
.
Model
==
""
{
audioRequest
.
Model
=
formData
.
Get
(
"model"
)
}
if
audioRequest
.
Model
==
""
{
return
nil
,
errors
.
New
(
"model is required"
)
}
audioRequest
.
ResponseFormat
=
formData
.
Get
(
"response_format"
)
if
audioRequest
.
ResponseFormat
==
""
{
audioRequest
.
ResponseFormat
=
"json"
}
...
...
service/token_counter.go
View file @
a00c6393
...
...
@@ -10,6 +10,7 @@ import (
_
"image/png"
"log"
"math"
"path/filepath"
"strings"
"sync"
"unicode/utf8"
...
...
@@ -18,6 +19,7 @@ import (
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
relaycommon
"github.com/QuantumNous/new-api/relay/common"
constant2
"github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
...
...
@@ -254,6 +256,10 @@ func getImageToken(fileMeta *types.FileMeta, model string, stream bool) (int, er
}
func
CountRequestToken
(
c
*
gin
.
Context
,
meta
*
types
.
TokenCountMeta
,
info
*
relaycommon
.
RelayInfo
)
(
int
,
error
)
{
if
meta
==
nil
{
return
0
,
errors
.
New
(
"token count meta is nil"
)
}
if
!
constant
.
GetMediaToken
{
return
0
,
nil
}
...
...
@@ -263,8 +269,29 @@ func CountRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *relayco
if
info
.
RelayFormat
==
types
.
RelayFormatOpenAIRealtime
{
return
0
,
nil
}
if
meta
==
nil
{
return
0
,
errors
.
New
(
"token count meta is nil"
)
if
info
.
RelayMode
==
constant2
.
RelayModeAudioTranscription
||
info
.
RelayMode
==
constant2
.
RelayModeAudioTranslation
{
multiForm
,
err
:=
common
.
ParseMultipartFormReusable
(
c
)
if
err
!=
nil
{
return
0
,
fmt
.
Errorf
(
"error parsing multipart form: %v"
,
err
)
}
fileHeaders
:=
multiForm
.
File
[
"file"
]
totalAudioToken
:=
0
for
_
,
fileHeader
:=
range
fileHeaders
{
file
,
err
:=
fileHeader
.
Open
()
if
err
!=
nil
{
return
0
,
fmt
.
Errorf
(
"error opening audio file: %v"
,
err
)
}
defer
file
.
Close
()
// get ext and io.seeker
ext
:=
filepath
.
Ext
(
fileHeader
.
Filename
)
duration
,
err
:=
common
.
GetAudioDuration
(
c
.
Request
.
Context
(),
file
,
ext
)
if
err
!=
nil
{
return
0
,
fmt
.
Errorf
(
"error getting audio duration: %v"
,
err
)
}
// 一分钟 1000 token,与 $price / minute 对齐
totalAudioToken
+=
int
(
math
.
Round
(
math
.
Ceil
(
duration
)
/
60.0
*
1000
))
}
return
totalAudioToken
,
nil
}
model
:=
common
.
GetContextKeyString
(
c
,
constant
.
ContextKeyOriginalModel
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment