fix(build): universal compile for CGO
This commit is contained in:
Vendored
+2
-1
@@ -52,7 +52,8 @@
|
||||
"variant": "cpp"
|
||||
},
|
||||
"go.toolsEnvVars": {
|
||||
"CGO_ENABLED": "1",
|
||||
"CGO_CFLAGS": "-I${workspaceFolder}/third_party/whisper/include",
|
||||
"CGO_LDFLAGS": "-L${workspaceFolder}/third_party/whisper/lib -lwhisper -lggml -lstdc++ -lm"
|
||||
"CGO_LDFLAGS": "-L${workspaceFolder}/third_party/whisper/lib"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ ifeq ($(VERSION),)
|
||||
VERSION := $(shell git describe --tags --always --dirty="-dev")
|
||||
endif
|
||||
LDFLAGS := -ldflags '-X "github.com/sjzar/chatlog/pkg/version.Version=$(VERSION)" -w -s'
|
||||
TAG := --tags "fts5"
|
||||
CGO_FLAG := CGO_ENABLED=1 CGO_CFLAGS="-I./third_party/whisper/include" CGO_LDFLAGS="-L./third_party/whisper/lib"
|
||||
|
||||
PLATFORMS := \
|
||||
darwin/amd64 \
|
||||
@@ -52,7 +54,7 @@ test:
|
||||
build:
|
||||
@echo "Building for current platform..."
|
||||
@$(MKDIR_BIN)
|
||||
@CGO_ENABLED=1 $(GO) build --tags "fts5" -trimpath $(LDFLAGS) -o bin/$(BINARY_NAME)$(BINARY_SUFFIX) main.go
|
||||
@$(CGO_FLAG) $(GO) build -trimpath $(LDFLAGS) $(TAG) -o bin/$(BINARY_NAME)$(BINARY_SUFFIX) main.go
|
||||
@echo "Done!"
|
||||
|
||||
crossbuild: clean
|
||||
@@ -67,7 +69,7 @@ crossbuild: clean
|
||||
[ "$$os" = "windows" ] && output_name=$$output_name.exe; \
|
||||
echo "Building for $$os/$$arch..."; \
|
||||
echo "Building for $$output_name..."; \
|
||||
@GOOS=$$os GOARCH=$$arch CGO_ENABLED=1 GOARM=$$float $(GO) build --tags "fts5" -trimpath $(LDFLAGS) -o $$output_name main.go ; \
|
||||
@GOOS=$$os GOARCH=$$arch GOARM=$$float $(CGO_FLAG) $(GO) build -trimpath $(LDFLAGS) $(TAG) -o $$output_name main.go ; \
|
||||
if [ "$(ENABLE_UPX)" = "1" ] && echo "$(UPX_PLATFORMS)" | grep -q "$$os/$$arch"; then \
|
||||
echo "Compressing binary $$output_name..." && upx --best $$output_name; \
|
||||
fi; \
|
||||
|
||||
@@ -5,7 +5,6 @@ type TUIConfig struct {
|
||||
LastAccount string `mapstructure:"last_account" json:"last_account"`
|
||||
History []ProcessConfig `mapstructure:"history" json:"history"`
|
||||
Webhook *Webhook `mapstructure:"webhook" json:"webhook"`
|
||||
Speech *SpeechConfig `mapstructure:"speech" json:"speech"`
|
||||
}
|
||||
|
||||
var TUIDefaults = map[string]any{}
|
||||
|
||||
@@ -186,10 +186,7 @@ func (c *Context) GetWebhook() *conf.Webhook {
|
||||
}
|
||||
|
||||
func (c *Context) GetSpeech() *conf.SpeechConfig {
|
||||
if c == nil || c.conf == nil {
|
||||
return nil
|
||||
}
|
||||
return c.conf.Speech
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Context) SetHTTPEnabled(enabled bool) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,18 +3,14 @@ package http
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/chatlog/conf"
|
||||
"github.com/sjzar/chatlog/internal/chatlog/database"
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/speech"
|
||||
"github.com/sjzar/chatlog/internal/speech/whispercpp"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -27,17 +23,11 @@ type Service struct {
|
||||
mcpServer *server.MCPServer
|
||||
mcpSSEServer *server.SSEServer
|
||||
mcpStreamableServer *server.StreamableHTTPServer
|
||||
|
||||
speechCfg *conf.SpeechConfig
|
||||
speechOpts speech.Options
|
||||
speech speech.Transcriber
|
||||
speechErr error
|
||||
}
|
||||
|
||||
type Config interface {
|
||||
GetHTTPAddr() string
|
||||
GetDataDir() string
|
||||
GetSpeech() *conf.SpeechConfig
|
||||
}
|
||||
|
||||
func NewService(conf Config, db *database.Service) *Service {
|
||||
@@ -65,7 +55,6 @@ func NewService(conf Config, db *database.Service) *Service {
|
||||
|
||||
s.initMCPServer()
|
||||
s.initRouter()
|
||||
s.initSpeech()
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -115,44 +104,9 @@ func (s *Service) Stop() error {
|
||||
}
|
||||
|
||||
log.Info().Msg("HTTP server stopped")
|
||||
s.shutdownSpeech()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetRouter() *gin.Engine {
|
||||
return s.router
|
||||
}
|
||||
|
||||
func (s *Service) initSpeech() {
|
||||
cfg := s.conf.GetSpeech()
|
||||
if cfg == nil || !cfg.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
modelPath := strings.TrimSpace(cfg.Model)
|
||||
if modelPath == "" {
|
||||
log.Warn().Msg("speech config enabled but model path is empty; disabling speech backend")
|
||||
return
|
||||
}
|
||||
|
||||
opts := cfg.ToOptions()
|
||||
transcriber, err := whispercpp.New(whispercpp.Config{ModelPath: modelPath, DefaultOptions: opts})
|
||||
if err != nil {
|
||||
log.Err(err).Str("model", modelPath).Msg("failed to initialize whisper backend; speech disabled")
|
||||
s.speechErr = err
|
||||
return
|
||||
}
|
||||
|
||||
s.speechCfg = cfg
|
||||
s.speechOpts = opts
|
||||
s.speech = transcriber
|
||||
log.Info().Str("model", modelPath).Msg("speech-to-text backend initialized")
|
||||
}
|
||||
|
||||
func (s *Service) shutdownSpeech() {
|
||||
if s.speech != nil {
|
||||
s.speech.Close()
|
||||
s.speech = nil
|
||||
log.Info().Msg("speech-to-text backend stopped")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,93 +365,6 @@
|
||||
color: var(--error-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.voice-message {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
background: rgba(46, 189, 240, 0.08);
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.voice-message .voice-stt-btn {
|
||||
background-color: #2d333b;
|
||||
border: 1px solid #4d6277;
|
||||
color: #d7e2f2;
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.voice-message .voice-stt-btn:hover {
|
||||
background-color: #3a4a5b;
|
||||
}
|
||||
|
||||
.voice-message .voice-stt-btn[disabled] {
|
||||
opacity: 0.65;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.voice-message .voice-transcript {
|
||||
font-size: 13px;
|
||||
color: #2c3e50;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.voice-message .voice-transcript[data-state="idle"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.voice-message .voice-text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.voice-message .voice-transcript .voice-segment {
|
||||
padding: 4px 0;
|
||||
border-left: 2px solid rgba(52, 152, 219, 0.45);
|
||||
margin-top: 4px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.voice-message .voice-transcript .voice-segment-time {
|
||||
font-size: 12px;
|
||||
color: #6c7a89;
|
||||
}
|
||||
|
||||
.voice-message .voice-segments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.voice-message .voice-meta,
|
||||
.voice-message .voice-transcript .voice-meta {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #6c7a89;
|
||||
}
|
||||
|
||||
.voice-message .voice-transcript .voice-segment span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.voice-message .voice-text.voice-empty {
|
||||
color: #95a5a6;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1261,7 +1174,6 @@
|
||||
resultContainer.innerHTML = `<pre class="result-block">${highlighted}</pre>`;
|
||||
}
|
||||
}
|
||||
prepareVoiceMessages(resultContainer);
|
||||
} catch (error) {
|
||||
resultContainer.innerHTML = "";
|
||||
errorMessage.textContent = `查询出错: ${error.message}`;
|
||||
@@ -1376,209 +1288,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
const voiceFallbackCache = new Map();
|
||||
|
||||
function prepareVoiceMessages(root) {
|
||||
if (!root) return;
|
||||
if (
|
||||
window.ChatlogSpeech &&
|
||||
typeof window.ChatlogSpeech.refreshVoiceMessages ===
|
||||
"function"
|
||||
) {
|
||||
window.ChatlogSpeech.refreshVoiceMessages();
|
||||
return;
|
||||
}
|
||||
fallbackEnsureVoiceState(root);
|
||||
}
|
||||
|
||||
function fallbackEnsureVoiceState(root) {
|
||||
root.querySelectorAll(".voice-message").forEach((container) => {
|
||||
const transcript =
|
||||
container.querySelector(".voice-transcript");
|
||||
if (transcript && !transcript.dataset.state) {
|
||||
transcript.dataset.state = "idle";
|
||||
}
|
||||
const btn = container.querySelector(".voice-stt-btn");
|
||||
if (btn && !btn.dataset.labelIdle) {
|
||||
btn.dataset.labelIdle = btn.textContent.trim();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function voiceKeyFromButton(btn) {
|
||||
const container = btn.closest(".voice-message");
|
||||
if (!container) return "";
|
||||
const link = container.querySelector(
|
||||
"a.voice-original, a[href]"
|
||||
);
|
||||
if (!link) return "";
|
||||
const href = link.getAttribute("href") || "";
|
||||
try {
|
||||
const url = new URL(href, window.location.href);
|
||||
return (
|
||||
url.searchParams.get("msgid") ||
|
||||
url.searchParams.get("id") ||
|
||||
url.pathname
|
||||
);
|
||||
} catch (_err) {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
async function fallbackVoiceRequest(btn) {
|
||||
const container = btn.closest(".voice-message");
|
||||
const transcript =
|
||||
container?.querySelector(".voice-transcript");
|
||||
const key = voiceKeyFromButton(btn);
|
||||
if (!container || !transcript || !key) return;
|
||||
|
||||
if (!transcript.dataset.state) {
|
||||
transcript.dataset.state = "idle";
|
||||
}
|
||||
|
||||
const cached = voiceFallbackCache.get(key);
|
||||
if (cached) {
|
||||
transcript.innerHTML = renderTranscript(cached);
|
||||
transcript.dataset.state = "ready";
|
||||
btn.disabled = false;
|
||||
btn.dataset.state = "done";
|
||||
btn.textContent = "查看文字";
|
||||
return;
|
||||
}
|
||||
|
||||
const originalText =
|
||||
btn.dataset.labelIdle || btn.textContent || "语音转文字";
|
||||
btn.dataset.labelIdle = originalText;
|
||||
btn.disabled = true;
|
||||
btn.dataset.state = "loading";
|
||||
btn.textContent = "转文字中…";
|
||||
|
||||
transcript.dataset.state = "loading";
|
||||
transcript.textContent = "语音识别进行中…";
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/v1/voice/transcribe", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ key }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const payload = await response.json();
|
||||
voiceFallbackCache.set(key, payload);
|
||||
transcript.innerHTML = renderTranscript(payload);
|
||||
transcript.dataset.state = "ready";
|
||||
btn.disabled = false;
|
||||
btn.textContent = "已转文字";
|
||||
btn.dataset.state = "done";
|
||||
} catch (err) {
|
||||
console.error("语音识别失败:", err);
|
||||
transcript.dataset.state = "error";
|
||||
transcript.textContent = `转文字失败: ${err.message}`;
|
||||
btn.textContent = "重试转文字";
|
||||
btn.dataset.state = "error";
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTranscript(data) {
|
||||
const pieces = [];
|
||||
const rawText =
|
||||
data && typeof data.text === "string"
|
||||
? data.text.trim()
|
||||
: "";
|
||||
if (rawText) {
|
||||
pieces.push(
|
||||
`<div class="voice-text">${escapeHtml(rawText)}</div>`
|
||||
);
|
||||
} else {
|
||||
pieces.push(
|
||||
'<div class="voice-text voice-empty">未识别到文本</div>'
|
||||
);
|
||||
}
|
||||
|
||||
const segments = Array.isArray(data && data.segments)
|
||||
? data.segments
|
||||
: [];
|
||||
if (segments.length) {
|
||||
const rendered = segments
|
||||
.map((segment) => {
|
||||
const start = formatSeconds(
|
||||
segment && segment.start
|
||||
);
|
||||
const end = formatSeconds(segment && segment.end);
|
||||
const caption =
|
||||
segment && segment.text
|
||||
? segment.text.trim()
|
||||
: "";
|
||||
return `<div class="voice-segment"><span class="voice-segment-time">${escapeHtml(
|
||||
start
|
||||
)} - ${escapeHtml(end)}</span><span>${escapeHtml(
|
||||
caption
|
||||
)}</span></div>`;
|
||||
})
|
||||
.join("");
|
||||
pieces.push(
|
||||
`<div class="voice-segments">${rendered}</div>`
|
||||
);
|
||||
}
|
||||
|
||||
const meta = [];
|
||||
if (data && data.language) {
|
||||
meta.push(`语言: ${escapeHtml(String(data.language))}`);
|
||||
}
|
||||
if (
|
||||
data &&
|
||||
typeof data.duration === "number" &&
|
||||
isFinite(data.duration) &&
|
||||
data.duration > 0
|
||||
) {
|
||||
meta.push(
|
||||
`音频: ${escapeHtml(formatSeconds(data.duration))}`
|
||||
);
|
||||
}
|
||||
if (meta.length) {
|
||||
pieces.push(
|
||||
`<div class="voice-meta">${meta.join(" · ")}</div>`
|
||||
);
|
||||
}
|
||||
|
||||
return pieces.join("");
|
||||
}
|
||||
|
||||
function formatSeconds(value) {
|
||||
const num = Number(value);
|
||||
if (!isFinite(num) || num <= 0) {
|
||||
return "0s";
|
||||
}
|
||||
if (num >= 60) {
|
||||
const m = Math.floor(num / 60);
|
||||
const s = Math.round(num % 60);
|
||||
return `${m}m${s < 10 ? "0" : ""}${s}s`;
|
||||
}
|
||||
if (num < 1) {
|
||||
return `${num.toFixed(2)}s`;
|
||||
}
|
||||
return `${Math.round(num)}s`;
|
||||
}
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
const btn = event.target.closest(".voice-stt-btn");
|
||||
if (!btn) return;
|
||||
if (
|
||||
window.ChatlogSpeech &&
|
||||
typeof window.ChatlogSpeech.requestVoice === "function"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
fallbackVoiceRequest(btn);
|
||||
});
|
||||
|
||||
const diaryDateInput = document.getElementById("diary-date");
|
||||
if (diaryDateInput) {
|
||||
const today = new Date();
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
//go:build cgo
|
||||
|
||||
package whispercpp
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -I${SRCDIR}/../../../third_party/whisper/include
|
||||
#cgo LDFLAGS: -L${SRCDIR}/../../../third_party/whisper/lib -lwhisper -lggml -lstdc++ -lm
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Reference in New Issue
Block a user