fix(build): universal compile for CGO

This commit is contained in:
Symb0x76
2025-10-10 20:53:54 +08:00
parent 39b2a56c18
commit 31b6b9824e
9 changed files with 11 additions and 882 deletions
+2 -1
View File
@@ -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 -2
View File
@@ -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; \
-1
View File
@@ -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{}
+1 -4
View File
@@ -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
-46
View File
@@ -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")
}
}
-291
View File
@@ -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 (
Binary file not shown.