mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 20:17:20 +00:00
fix(tools): correct media provider params and UI fixes
- MiniMax audio: fix invalid params (sample_rate/bitrate as int, auto instrumental when no lyrics, pass duration_seconds) - MiniMax/DashScope image: map aspect_ratio to provider size format - Gemini video: read person_generation from chain params instead of hardcode - GetParamInt: add string-to-int coercion for UI select values - UI: fix combobox portal selection bug, dropdown overflow clipping, provider chain form spacing, skill dialog min-height - Update bitrate options to numeric bps values in params schema
This commit is contained in:
1 parent
157d09c021
commit
9d0af657e5
9 files changed
+83
-17
No files matched your search
@@ -19,8 +19,15 @@ func callMinimaxMusicGen(ctx context.Context, apiKey, apiBase, model, prompt str
|
||||
lyrics := GetParamString(params, "lyrics", "")
|
||||
instrumental := GetParamBool(params, "instrumental", false)
|
||||
lyricsOptimizer := GetParamBool(params, "lyrics_optimizer", false)
|
||||
sampleRate := GetParamString(params, "sample_rate", "44100")
|
||||
bitrate := GetParamString(params, "bitrate", "320k")
|
||||
sampleRate := GetParamInt(params, "sample_rate", 44100)
|
||||
bitrate := GetParamInt(params, "bitrate", 256000)
|
||||
|
||||
// lyrics is required when is_instrumental is false
|
||||
if !instrumental && lyrics == "" {
|
||||
instrumental = true
|
||||
}
|
||||
|
||||
duration := GetParamInt(params, "duration", 0)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"model": model,
|
||||
@@ -37,6 +44,9 @@ func callMinimaxMusicGen(ctx context.Context, apiKey, apiBase, model, prompt str
|
||||
if lyrics != "" {
|
||||
body["lyrics"] = lyrics
|
||||
}
|
||||
if duration > 0 {
|
||||
body["duration_seconds"] = duration
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
|
||||
@@ -59,8 +59,25 @@ func dashScopeTaskEndpoint(apiBase, taskID string) string {
|
||||
// callDashScopeImageGen calls the DashScope (Alibaba/Bailian) multimodal image generation API.
|
||||
// The API is async: an initial POST returns a task_id, which is then polled until done.
|
||||
// On completion, output.results[].url contains the image URL to download.
|
||||
// aspectRatioToDashScopeSize converts aspect_ratio to DashScope size format.
|
||||
// Falls back to explicit "size" param if set, otherwise uses aspect_ratio mapping.
|
||||
func aspectRatioToDashScopeSize(params map[string]any) string {
|
||||
if s := GetParamString(params, "size", ""); s != "" {
|
||||
return s
|
||||
}
|
||||
ar := GetParamString(params, "aspect_ratio", "1:1")
|
||||
switch ar {
|
||||
case "16:9":
|
||||
return "1280*720"
|
||||
case "9:16":
|
||||
return "720*1280"
|
||||
default:
|
||||
return "1024*1024"
|
||||
}
|
||||
}
|
||||
|
||||
func callDashScopeImageGen(ctx context.Context, apiKey, apiBase, model, prompt string, params map[string]any) ([]byte, *providers.Usage, error) {
|
||||
size := GetParamString(params, "size", "1024*1024")
|
||||
size := aspectRatioToDashScopeSize(params)
|
||||
promptExtend := GetParamBool(params, "prompt_extend", true)
|
||||
|
||||
endpoint := dashScopeImageEndpoint(apiBase)
|
||||
|
||||
@@ -16,8 +16,30 @@ import (
|
||||
// callMinimaxImageGen calls the MiniMax image generation API.
|
||||
// Endpoint: POST {apiBase}/image_generation
|
||||
// Response: base64 image data in data.image_list[0].base64_image
|
||||
// aspectRatioToMinimaxSize converts aspect_ratio (e.g. "16:9") to MiniMax size format.
|
||||
// Falls back to explicit "size" param if set, otherwise uses aspect_ratio mapping.
|
||||
func aspectRatioToMinimaxSize(params map[string]any) string {
|
||||
// Explicit size param takes priority (from UI provider settings)
|
||||
if s := GetParamString(params, "size", ""); s != "" {
|
||||
return s
|
||||
}
|
||||
ar := GetParamString(params, "aspect_ratio", "1:1")
|
||||
switch ar {
|
||||
case "16:9":
|
||||
return "1280*720"
|
||||
case "9:16":
|
||||
return "720*1280"
|
||||
case "4:3":
|
||||
return "1024*768"
|
||||
case "3:4":
|
||||
return "768*1024"
|
||||
default:
|
||||
return "1024*1024"
|
||||
}
|
||||
}
|
||||
|
||||
func callMinimaxImageGen(ctx context.Context, apiKey, apiBase, model, prompt string, params map[string]any) ([]byte, *providers.Usage, error) {
|
||||
size := GetParamString(params, "size", "1024*1024")
|
||||
size := aspectRatioToMinimaxSize(params)
|
||||
promptOptimizer := GetParamBool(params, "prompt_optimizer", true)
|
||||
|
||||
body := map[string]interface{}{
|
||||
|
||||
@@ -149,7 +149,7 @@ func (t *CreateVideoTool) callProvider(ctx context.Context, cp credentialProvide
|
||||
|
||||
switch ProviderTypeFromName(providerName) {
|
||||
case "gemini":
|
||||
return t.callGeminiVideoGen(ctx, cp.APIKey(), cp.APIBase(), model, prompt, duration, aspectRatio)
|
||||
return t.callGeminiVideoGen(ctx, cp.APIKey(), cp.APIBase(), model, prompt, duration, aspectRatio, params)
|
||||
case "minimax":
|
||||
return callMinimaxVideoGen(ctx, cp.APIKey(), cp.APIBase(), model, params)
|
||||
default:
|
||||
@@ -159,7 +159,7 @@ func (t *CreateVideoTool) callProvider(ctx context.Context, cp credentialProvide
|
||||
|
||||
// callGeminiVideoGen uses the Gemini predictLongRunning API for Veo video generation.
|
||||
// Flow: POST predictLongRunning → poll operation → download video from URI.
|
||||
func (t *CreateVideoTool) callGeminiVideoGen(ctx context.Context, apiKey, apiBase, model, prompt string, duration int, aspectRatio string) ([]byte, *providers.Usage, error) {
|
||||
func (t *CreateVideoTool) callGeminiVideoGen(ctx context.Context, apiKey, apiBase, model, prompt string, duration int, aspectRatio string, params map[string]any) ([]byte, *providers.Usage, error) {
|
||||
nativeBase := strings.TrimRight(apiBase, "/")
|
||||
nativeBase = strings.TrimSuffix(nativeBase, "/openai")
|
||||
|
||||
@@ -173,7 +173,7 @@ func (t *CreateVideoTool) callGeminiVideoGen(ctx context.Context, apiKey, apiBas
|
||||
"parameters": map[string]interface{}{
|
||||
"aspectRatio": aspectRatio,
|
||||
"durationSeconds": duration,
|
||||
"personGeneration": "allow_all",
|
||||
"personGeneration": GetParamString(params, "person_generation", "allow_all"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -297,6 +298,10 @@ func GetParamInt(params map[string]any, key string, fallback int) int {
|
||||
return int(v)
|
||||
case int:
|
||||
return v
|
||||
case string:
|
||||
if n, err := strconv.Atoi(v); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -319,6 +324,8 @@ func ProviderTypeFromName(name string) string {
|
||||
return "anthropic"
|
||||
case name == "suno" || strings.HasPrefix(name, "suno"):
|
||||
return "suno"
|
||||
case name == "yescale":
|
||||
return "openai"
|
||||
default:
|
||||
return "openai_compat"
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export function Combobox({
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [search, setSearch] = React.useState("");
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||
const [dropdownStyle, setDropdownStyle] = React.useState<React.CSSProperties>({});
|
||||
|
||||
// Sync search text when value changes externally — show label if available
|
||||
@@ -41,7 +42,11 @@ export function Combobox({
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
const target = e.target as Node;
|
||||
if (
|
||||
containerRef.current && !containerRef.current.contains(target) &&
|
||||
(!dropdownRef.current || !dropdownRef.current.contains(target))
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
@@ -54,11 +59,14 @@ export function Combobox({
|
||||
if (!open || !portalContainer?.current || !containerRef.current) return;
|
||||
const inputRect = containerRef.current.getBoundingClientRect();
|
||||
const portalRect = portalContainer.current.getBoundingClientRect();
|
||||
const left = inputRect.left - portalRect.left;
|
||||
const maxWidth = portalRect.width - left;
|
||||
setDropdownStyle({
|
||||
position: "absolute",
|
||||
top: inputRect.bottom - portalRect.top + 4,
|
||||
left: inputRect.left - portalRect.left,
|
||||
left,
|
||||
width: inputRect.width,
|
||||
maxWidth,
|
||||
zIndex: 50,
|
||||
});
|
||||
}, [open, search, portalContainer]);
|
||||
@@ -89,10 +97,11 @@ export function Combobox({
|
||||
|
||||
const dropdownContent = open && filtered.length > 0 && (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
style={portalContainer ? dropdownStyle : undefined}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground max-h-60 min-w-full overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
!portalContainer && "absolute top-full left-0 z-50 mt-1",
|
||||
"bg-popover text-popover-foreground max-h-60 overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
portalContainer ? "overflow-hidden" : "absolute top-full left-0 z-50 mt-1 min-w-full",
|
||||
)}
|
||||
>
|
||||
{filtered.map((o) => (
|
||||
|
||||
@@ -425,7 +425,7 @@ export function MediaProviderChainForm({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2 max-h-[60vh] overflow-y-auto pr-1">
|
||||
<div className="space-y-2 max-h-[60vh] overflow-y-auto pr-1 my-4">
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={entries.map((e) => e.id)} strategy={verticalListSortingStrategy}>
|
||||
{entries.map((entry, index) => (
|
||||
|
||||
@@ -101,11 +101,12 @@ export const MEDIA_PARAMS_SCHEMA: Record<string, Record<string, ParamField[]>> =
|
||||
key: "bitrate",
|
||||
label: "Bitrate",
|
||||
type: "select",
|
||||
default: "320k",
|
||||
default: "256000",
|
||||
options: [
|
||||
{ value: "128k", label: "128k" },
|
||||
{ value: "256k", label: "256k" },
|
||||
{ value: "320k", label: "320k" },
|
||||
{ value: "32000", label: "32kbps" },
|
||||
{ value: "64000", label: "64kbps" },
|
||||
{ value: "128000", label: "128kbps" },
|
||||
{ value: "256000", label: "256kbps" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -100,7 +100,7 @@ export function SkillDetailDialog({
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={() => onClose()}>
|
||||
<DialogContent className="max-h-[85vh] overflow-hidden flex flex-col sm:max-w-2xl md:max-w-4xl lg:max-w-5xl xl:max-w-6xl 2xl:max-w-7xl">
|
||||
<DialogContent className="max-h-[85vh] md:min-h-[60vh] overflow-hidden flex flex-col sm:max-w-2xl md:max-w-4xl lg:max-w-5xl xl:max-w-6xl 2xl:max-w-7xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 flex-wrap">
|
||||
{skill.name}
|
||||
|
||||
Reference in new issue
Block a user