smart_prompt 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/config/siliconflow_config.yml +95 -0
- data/examples/siliconflow_example.rb +175 -0
- data/lib/smart_prompt/adapters/siliconflow/embed.rb +33 -0
- data/lib/smart_prompt/adapters/siliconflow/image.rb +103 -0
- data/lib/smart_prompt/adapters/siliconflow/rerank.rb +41 -0
- data/lib/smart_prompt/adapters/siliconflow/text.rb +54 -0
- data/lib/smart_prompt/adapters/siliconflow/video.rb +111 -0
- data/lib/smart_prompt/adapters/siliconflow/voice.rb +102 -0
- data/lib/smart_prompt/adapters/zhipu/embed.rb +32 -0
- data/lib/smart_prompt/adapters/zhipu/image.rb +59 -0
- data/lib/smart_prompt/adapters/zhipu/rerank.rb +17 -0
- data/lib/smart_prompt/adapters/zhipu/text.rb +57 -0
- data/lib/smart_prompt/adapters/zhipu/video.rb +101 -0
- data/lib/smart_prompt/adapters/zhipu/voice.rb +55 -0
- data/lib/smart_prompt/concerns/http_client.rb +147 -0
- data/lib/smart_prompt/concerns/image_persistence.rb +62 -0
- data/lib/smart_prompt/concerns/multimodal_messages.rb +108 -0
- data/lib/smart_prompt/concerns/openai_chat_shaping.rb +87 -0
- data/lib/smart_prompt/siliconflow_adapter.rb +91 -0
- data/lib/smart_prompt/version.rb +1 -1
- data/workers/siliconflow_workers.rb +167 -0
- metadata +21 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module SmartPrompt
|
|
6
|
+
# Shared multimodal-message normalization for Net::HTTP adapters (ZhipuAI, SenseNova,
|
|
7
|
+
# SiliconFlow). Turns an OpenAI-style content array into the shape the provider expects,
|
|
8
|
+
# inlining local image/audio/video files as base64 data URLs and passing http(s)/data
|
|
9
|
+
# URLs through. Each adapter previously carried a near-identical copy of this logic.
|
|
10
|
+
#
|
|
11
|
+
# SiliconFlow's variant is the superset (image_url + video_url + audio_url, preserving
|
|
12
|
+
# detail/max_frames/fps); Zhipu/SenseNova only ever send image_url, which is a subset.
|
|
13
|
+
module MultimodalMessages
|
|
14
|
+
SUPPORTED_IMAGE_FORMATS = %w[jpg jpeg png gif bmp webp].freeze
|
|
15
|
+
|
|
16
|
+
def process_multimodal_messages(messages)
|
|
17
|
+
messages.map do |msg|
|
|
18
|
+
role = msg[:role] || msg["role"]
|
|
19
|
+
content = msg[:content] || msg["content"]
|
|
20
|
+
content = content.map { |item| normalize_content_item(item) } if content.is_a?(Array)
|
|
21
|
+
{ "role" => role, "content" => content }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def normalize_content_item(item)
|
|
26
|
+
return { "type" => "text", "text" => item.to_s } unless item.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
case item[:type] || item["type"]
|
|
29
|
+
when "image_url"
|
|
30
|
+
normalize_media_part(item, "image_url", :image)
|
|
31
|
+
when "video_url"
|
|
32
|
+
normalize_media_part(item, "video_url", :video)
|
|
33
|
+
when "audio_url"
|
|
34
|
+
normalize_media_part(item, "audio_url", :audio)
|
|
35
|
+
else
|
|
36
|
+
stringify_hash(item)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Build an image_url/video_url/audio_url part, inlining local files as data URLs and
|
|
41
|
+
# preserving any extra keys (detail, max_frames, fps) on the media hash.
|
|
42
|
+
def normalize_media_part(item, type, media_kind)
|
|
43
|
+
iu = item[type.to_sym] || item[type]
|
|
44
|
+
if iu.is_a?(Hash)
|
|
45
|
+
url = iu[:url] || iu["url"]
|
|
46
|
+
part = { "type" => type, type => { "url" => normalize_media_url(url, media_kind) } }
|
|
47
|
+
iu.each { |k, v| part[type][k.to_s] = stringify_hash(v) unless k.to_s == "url" }
|
|
48
|
+
part
|
|
49
|
+
else
|
|
50
|
+
{ "type" => type, type => { "url" => normalize_media_url(iu, media_kind) } }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Resolve a media URL embedded in a message: http(s)/data pass through; a local path
|
|
55
|
+
# is base64-encoded as a data URL.
|
|
56
|
+
def normalize_media_url(url, kind = :image)
|
|
57
|
+
return url if url.nil?
|
|
58
|
+
return url if url.start_with?("http://", "https://", "data:")
|
|
59
|
+
|
|
60
|
+
label = kind == :image ? "Image" : kind.to_s.capitalize
|
|
61
|
+
raise Error, "#{label} file not found: #{url}" unless File.exist?(url)
|
|
62
|
+
ext = File.extname(url).downcase.delete(".")
|
|
63
|
+
case kind
|
|
64
|
+
when :image
|
|
65
|
+
raise Error, "Unsupported image format: #{ext}" unless SUPPORTED_IMAGE_FORMATS.include?(ext)
|
|
66
|
+
mime = ext == "jpg" ? "jpeg" : ext
|
|
67
|
+
"data:image/#{mime};base64,#{Base64.strict_encode64(File.binread(url))}"
|
|
68
|
+
when :audio
|
|
69
|
+
"data:audio/#{ext.empty? ? 'wav' : ext};base64,#{Base64.strict_encode64(File.binread(url))}"
|
|
70
|
+
when :video
|
|
71
|
+
"data:video/#{ext.empty? ? 'mp4' : ext};base64,#{Base64.strict_encode64(File.binread(url))}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Single-arg image-only shim (call sites like generate_video pass a plain image URL).
|
|
76
|
+
def normalize_image_url(url)
|
|
77
|
+
normalize_media_url(url, :image)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Accept a local path, a base64 data URL, or an http(s) URL for image-edit /
|
|
81
|
+
# image-to-video `image` fields.
|
|
82
|
+
def normalize_input_image(image)
|
|
83
|
+
return image if image.nil?
|
|
84
|
+
|
|
85
|
+
if image.is_a?(String)
|
|
86
|
+
return image if image.start_with?("data:")
|
|
87
|
+
return image if image.start_with?("http://", "https://")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
raise Error, "Image file not found: #{image}" unless File.exist?(image)
|
|
91
|
+
ext = File.extname(image).downcase.delete(".")
|
|
92
|
+
raise Error, "Unsupported image format: #{ext}" unless SUPPORTED_IMAGE_FORMATS.include?(ext)
|
|
93
|
+
mime = ext == "jpg" ? "jpeg" : ext
|
|
94
|
+
"data:image/#{mime};base64,#{Base64.strict_encode64(File.binread(image))}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def stringify_hash(hash)
|
|
98
|
+
case hash
|
|
99
|
+
when Hash
|
|
100
|
+
hash.each_with_object({}) { |(k, v), memo| memo[k.to_s] = stringify_hash(v) }
|
|
101
|
+
when Array
|
|
102
|
+
hash.map { |v| stringify_hash(v) }
|
|
103
|
+
else
|
|
104
|
+
hash
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module SmartPrompt
|
|
2
|
+
# Shared shaping of Net::HTTP chat responses into the OpenAI completion / stream
|
|
3
|
+
# shape that the rest of SmartPrompt (Engine#@stream_proc, Conversation) expects.
|
|
4
|
+
#
|
|
5
|
+
# Reasoning models expose a thinking trace under a provider-specific field —
|
|
6
|
+
# surfaced here uniformly as `reasoning_content`. Adapters override one hook:
|
|
7
|
+
#
|
|
8
|
+
# reasoning_field_name — the source field on message/delta (default
|
|
9
|
+
# "reasoning_content"; SenseNova uses "reasoning"). Its value is remapped to
|
|
10
|
+
# reasoning_content so Engine#@stream_proc needs no per-provider logic.
|
|
11
|
+
#
|
|
12
|
+
# extra_top_level_fields(raw) — extra top-level keys to copy onto the shaped
|
|
13
|
+
# response/chunk (default {}; SenseNova adds system_fingerprint).
|
|
14
|
+
module OpenAIChatShaping
|
|
15
|
+
def build_completion_response(raw)
|
|
16
|
+
msg = raw.dig("choices", 0, "message") || {}
|
|
17
|
+
message = { "role" => msg["role"] || "assistant" }
|
|
18
|
+
message["content"] = msg["content"]
|
|
19
|
+
reasoning = msg[reasoning_field_name]
|
|
20
|
+
message["reasoning_content"] = reasoning if reasoning
|
|
21
|
+
message["tool_calls"] = msg["tool_calls"] if msg["tool_calls"]
|
|
22
|
+
|
|
23
|
+
response = {
|
|
24
|
+
"id" => raw["id"],
|
|
25
|
+
"object" => raw["object"] || "chat.completion",
|
|
26
|
+
"created" => raw["created"],
|
|
27
|
+
"model" => raw["model"],
|
|
28
|
+
"choices" => [{
|
|
29
|
+
"index" => 0,
|
|
30
|
+
"message" => message,
|
|
31
|
+
"finish_reason" => raw.dig("choices", 0, "finish_reason"),
|
|
32
|
+
}],
|
|
33
|
+
}
|
|
34
|
+
response["usage"] = raw["usage"] if raw["usage"]
|
|
35
|
+
merge_extra_top_level(response, raw)
|
|
36
|
+
response
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_stream_chunk(data)
|
|
40
|
+
chunk = {
|
|
41
|
+
"id" => data["id"],
|
|
42
|
+
"object" => data["object"],
|
|
43
|
+
"created" => data["created"],
|
|
44
|
+
"model" => data["model"],
|
|
45
|
+
}
|
|
46
|
+
chunk["usage"] = data["usage"] if data["usage"]
|
|
47
|
+
merge_extra_top_level(chunk, data)
|
|
48
|
+
|
|
49
|
+
choices = data["choices"] || []
|
|
50
|
+
if choices.any?
|
|
51
|
+
delta = choices[0]["delta"] || {}
|
|
52
|
+
new_delta = {}
|
|
53
|
+
new_delta["role"] = delta["role"] if delta["role"]
|
|
54
|
+
new_delta["content"] = delta["content"] if delta["content"]
|
|
55
|
+
reasoning = delta[reasoning_field_name]
|
|
56
|
+
new_delta["reasoning_content"] = reasoning if reasoning
|
|
57
|
+
new_delta["tool_calls"] = delta["tool_calls"] if delta["tool_calls"]
|
|
58
|
+
chunk["choices"] = [{
|
|
59
|
+
"index" => choices[0]["index"] || 0,
|
|
60
|
+
"delta" => new_delta,
|
|
61
|
+
"finish_reason" => choices[0]["finish_reason"],
|
|
62
|
+
}]
|
|
63
|
+
else
|
|
64
|
+
chunk["choices"] = []
|
|
65
|
+
end
|
|
66
|
+
chunk
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# ---- hooks (override in adapter) -----------------------------------------
|
|
70
|
+
|
|
71
|
+
def reasoning_field_name
|
|
72
|
+
"reasoning_content"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def extra_top_level_fields(_raw)
|
|
76
|
+
{}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def merge_extra_top_level(target, raw)
|
|
82
|
+
extra_top_level_fields(raw).each do |k, v|
|
|
83
|
+
target[k] = v unless v.nil? || target.key?(k)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
require "json"
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require_relative "concerns/image_persistence"
|
|
7
|
+
require_relative "concerns/openai_chat_shaping"
|
|
8
|
+
require_relative "concerns/multimodal_messages"
|
|
9
|
+
require_relative "concerns/http_client"
|
|
10
|
+
require_relative "adapters/siliconflow/text"
|
|
11
|
+
require_relative "adapters/siliconflow/embed"
|
|
12
|
+
require_relative "adapters/siliconflow/image"
|
|
13
|
+
require_relative "adapters/siliconflow/video"
|
|
14
|
+
require_relative "adapters/siliconflow/voice"
|
|
15
|
+
require_relative "adapters/siliconflow/rerank"
|
|
16
|
+
|
|
17
|
+
module SmartPrompt
|
|
18
|
+
# Adapter for 硅基流动 (SiliconFlow / SiliconCloud) — one adapter owns the whole
|
|
19
|
+
# provider: every category shares the base URL https://api.siliconflow.cn/v1 and
|
|
20
|
+
# Bearer auth.
|
|
21
|
+
#
|
|
22
|
+
# Per-modality behavior lives in capability modules under adapters/siliconflow/
|
|
23
|
+
# (Text / Embed / Image / Video / Voice / Rerank); cross-provider plumbing (HTTP,
|
|
24
|
+
# multimodal normalization, chat shaping, image saving) comes from the shared
|
|
25
|
+
# concerns. This class wires them together + holds config/credentials.
|
|
26
|
+
#
|
|
27
|
+
# Provider-specific quirks (all vs https://docs.siliconflow.cn/cn/api-reference):
|
|
28
|
+
# chat/vision — POST {base}/chat/completions (reasoning_content, no remap)
|
|
29
|
+
# embeddings — POST {base}/embeddings (dimensions only for Qwen3-Embedding)
|
|
30
|
+
# rerank — POST {base}/rerank (results[].relevance_score)
|
|
31
|
+
# image/edit — POST {base}/images/generations (images[].url; image_size/batch_size/guidance_scale)
|
|
32
|
+
# video — POST {base}/video/submit -> POST {base}/video/status (async; results.videos[].url)
|
|
33
|
+
# tts — POST {base}/audio/speech (binary audio response)
|
|
34
|
+
# asr — POST {base}/audio/transcriptions (multipart, field "file")
|
|
35
|
+
# voice — /uploads/audio/voice, /audio/voice/list, /audio/voice/deletions
|
|
36
|
+
class SiliconFlowAdapter < LLMAdapter
|
|
37
|
+
DEFAULT_BASE_URL = "https://api.siliconflow.cn/v1".freeze
|
|
38
|
+
|
|
39
|
+
# Cross-provider shared concerns
|
|
40
|
+
include ImagePersistence
|
|
41
|
+
include OpenAIChatShaping
|
|
42
|
+
include MultimodalMessages
|
|
43
|
+
include HTTPClient
|
|
44
|
+
|
|
45
|
+
# Per-capability modules
|
|
46
|
+
include SiliconFlow::Text
|
|
47
|
+
include SiliconFlow::Embed
|
|
48
|
+
include SiliconFlow::Image
|
|
49
|
+
include SiliconFlow::Video
|
|
50
|
+
include SiliconFlow::Voice
|
|
51
|
+
include SiliconFlow::Rerank
|
|
52
|
+
|
|
53
|
+
# ---- hooks for shared concerns -------------------------------------------
|
|
54
|
+
def provider_label
|
|
55
|
+
"SiliconFlow"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def default_image_prefix
|
|
59
|
+
"siliconflow_image"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def initialize(config)
|
|
63
|
+
super
|
|
64
|
+
SmartPrompt.logger.info "Start create the SmartPrompt SiliconFlowAdapter."
|
|
65
|
+
|
|
66
|
+
api_key = @config["api_key"]
|
|
67
|
+
if api_key.is_a?(String) && api_key.start_with?("ENV[") && api_key.end_with?("]")
|
|
68
|
+
api_key = eval(api_key)
|
|
69
|
+
end
|
|
70
|
+
# Tolerate a missing key at construction (e.g. when the ENV var isn't set yet)
|
|
71
|
+
# and let the first request fail with a clear auth error.
|
|
72
|
+
SmartPrompt.logger.warn "SiliconFlow api_key is empty — API calls will fail until it is set." if api_key.nil? || api_key.to_s.strip.empty?
|
|
73
|
+
|
|
74
|
+
@api_key = api_key
|
|
75
|
+
@base_url = (@config["url"] || DEFAULT_BASE_URL).to_s.chomp("/")
|
|
76
|
+
# Optional per-method URL overrides (default to the standard paths off @base_url).
|
|
77
|
+
@image_url = (@config["image_url"] || "#{@base_url}/images/generations").to_s
|
|
78
|
+
@video_submit_url = (@config["video_submit_url"] || "#{@base_url}/video/submit").to_s
|
|
79
|
+
@video_status_url = (@config["video_status_url"] || "#{@base_url}/video/status").to_s
|
|
80
|
+
@speech_url = (@config["speech_url"] || "#{@base_url}/audio/speech").to_s
|
|
81
|
+
@transcription_url = (@config["transcription_url"] || "#{@base_url}/audio/transcriptions").to_s
|
|
82
|
+
@voice_upload_url = (@config["voice_upload_url"] || "#{@base_url}/uploads/audio/voice").to_s
|
|
83
|
+
@voice_list_url = (@config["voice_list_url"] || "#{@base_url}/audio/voice/list").to_s
|
|
84
|
+
@voice_delete_url = (@config["voice_delete_url"] || "#{@base_url}/audio/voice/deletions").to_s
|
|
85
|
+
SmartPrompt.logger.info "SiliconFlow base_url=#{@base_url}"
|
|
86
|
+
rescue => e
|
|
87
|
+
SmartPrompt.logger.error "Failed to initialize SiliconFlow client: #{e.message}"
|
|
88
|
+
raise e.is_a?(SmartPrompt::Error) ? e : LLMAPIError, "Invalid SiliconFlow configuration: #{e.message}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/smart_prompt/version.rb
CHANGED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# 硅基流动 (SiliconFlow / SiliconCloud) workers for SmartPrompt
|
|
2
|
+
#
|
|
3
|
+
# One worker per model category, reusing the standard DSL (`use`, `model`, `sys_msg`,
|
|
4
|
+
# `prompt`, `send_msg`) and the media helpers. Chat/vision/embed/image/image-edit go
|
|
5
|
+
# through Conversation-delegated methods; rerank/video/tts/asr reach the adapter
|
|
6
|
+
# directly via engine.llms[...] (the methods Conversation does not delegate).
|
|
7
|
+
#
|
|
8
|
+
# `send_msg` transparently becomes streaming when the engine invokes the worker via
|
|
9
|
+
# call_worker_by_stream — so :siliconflow_chat serves both sync and stream callers.
|
|
10
|
+
|
|
11
|
+
# 1. 文本对话 (sync + stream)
|
|
12
|
+
SmartPrompt.define_worker :siliconflow_chat do
|
|
13
|
+
use "sf_chat"
|
|
14
|
+
model params[:model] if params[:model]
|
|
15
|
+
sys_msg(params[:system] || "你是一个有帮助的中文助手,回答简洁准确。", params)
|
|
16
|
+
prompt(params[:prompt] || "你好,请用一句话介绍硅基流动 SiliconFlow。")
|
|
17
|
+
send_msg
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 2. 多模态对话 (vision / video / audio). Accepts image_url/video_url/audio_url,
|
|
21
|
+
# a single media url, or arrays (image_urls).
|
|
22
|
+
SmartPrompt.define_worker :siliconflow_vision do
|
|
23
|
+
use "sf_vision"
|
|
24
|
+
model params[:model] if params[:model]
|
|
25
|
+
sys_msg("你是一个专业的多模态分析助手,能够准确描述和分析图像/视频/音频内容。", params)
|
|
26
|
+
|
|
27
|
+
content = [{ type: "text", text: params[:question] || "请描述这张图片中的内容。" }]
|
|
28
|
+
([params[:image_url]] + (params[:image_urls] || [])).compact.uniq.each do |url|
|
|
29
|
+
content << { type: "image_url", image_url: { url: url } }
|
|
30
|
+
end
|
|
31
|
+
content << { type: "video_url", video_url: { url: params[:video_url] } } if params[:video_url]
|
|
32
|
+
content << { type: "audio_url", audio_url: { url: params[:audio_url] } } if params[:audio_url]
|
|
33
|
+
add_message({ role: "user", content: content })
|
|
34
|
+
|
|
35
|
+
send_msg
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# 3. 向量模型 (embeddings). Returns a normalized numeric vector of the user text.
|
|
39
|
+
SmartPrompt.define_worker :siliconflow_embed do
|
|
40
|
+
use "sf_embed"
|
|
41
|
+
model params[:model] if params[:model]
|
|
42
|
+
prompt(params[:text] || "硅基流动 SiliconFlow 大模型")
|
|
43
|
+
embeddings(params[:length] || 1024)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 4. 重排 (rerank). Reorders params[:documents] by relevance to params[:query].
|
|
47
|
+
# Conversation does not delegate rerank, so we reach the adapter directly.
|
|
48
|
+
SmartPrompt.define_worker :siliconflow_rerank do
|
|
49
|
+
use "sf_rerank"
|
|
50
|
+
model params[:model] if params[:model]
|
|
51
|
+
adapter = engine.llms["sf_rerank"]
|
|
52
|
+
|
|
53
|
+
adapter.rerank(
|
|
54
|
+
params[:query],
|
|
55
|
+
params[:documents] || [],
|
|
56
|
+
model: params[:model],
|
|
57
|
+
top_n: params[:top_n],
|
|
58
|
+
return_documents: params[:return_documents],
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# 5. 文生图 (text-to-image). Returns the generated image(s); optionally saves to disk.
|
|
63
|
+
SmartPrompt.define_worker :siliconflow_image do
|
|
64
|
+
use "sf_image"
|
|
65
|
+
model params[:model] if params[:model]
|
|
66
|
+
|
|
67
|
+
images = generate_image(params[:prompt], {
|
|
68
|
+
model: params[:model],
|
|
69
|
+
negative_prompt: params[:negative_prompt],
|
|
70
|
+
image_size: params[:image_size] || params[:size],
|
|
71
|
+
batch_size: params[:batch_size] || params[:n],
|
|
72
|
+
seed: params[:seed],
|
|
73
|
+
num_inference_steps: params[:num_inference_steps],
|
|
74
|
+
guidance_scale: params[:guidance_scale],
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if params[:save_to_file]
|
|
78
|
+
saved = save_image(images, params[:output_dir] || "./generated_images", params[:filename_prefix] || "siliconflow")
|
|
79
|
+
{ images: images, saved_files: saved }
|
|
80
|
+
else
|
|
81
|
+
images
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 6. 图像编辑 / 图生图 (Qwen-Image-Edit). Accepts image (and image2/image3 for
|
|
86
|
+
# multi-image fusion) as local path, data URL, or http URL.
|
|
87
|
+
SmartPrompt.define_worker :siliconflow_image_edit do
|
|
88
|
+
use "sf_image"
|
|
89
|
+
model params[:model] || "Qwen/Qwen-Image-Edit-2509"
|
|
90
|
+
|
|
91
|
+
images = edit_image(params[:prompt], {
|
|
92
|
+
model: params[:model] || "Qwen/Qwen-Image-Edit-2509",
|
|
93
|
+
image: params[:image] || params[:image_file],
|
|
94
|
+
image2: params[:image2],
|
|
95
|
+
image3: params[:image3],
|
|
96
|
+
negative_prompt: params[:negative_prompt],
|
|
97
|
+
seed: params[:seed],
|
|
98
|
+
guidance_scale: params[:guidance_scale],
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
if params[:save_to_file]
|
|
102
|
+
saved = save_image(images, params[:output_dir] || "./edited_images", params[:filename_prefix] || "siliconflow_edit")
|
|
103
|
+
{ images: images, saved_files: saved }
|
|
104
|
+
else
|
|
105
|
+
images
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# 7. 文生视频 / 图生视频 (async: submit -> poll -> download).
|
|
110
|
+
SmartPrompt.define_worker :siliconflow_video do
|
|
111
|
+
use "sf_video"
|
|
112
|
+
model params[:model] if params[:model]
|
|
113
|
+
adapter = engine.llms["sf_video"]
|
|
114
|
+
|
|
115
|
+
submitted = adapter.generate_video(params[:prompt], params)
|
|
116
|
+
result = { submitted: submitted }
|
|
117
|
+
|
|
118
|
+
if params[:wait_for_completion]
|
|
119
|
+
completed = adapter.wait_for_video_completion(
|
|
120
|
+
submitted[:request_id],
|
|
121
|
+
check_interval: params[:check_interval] || 10,
|
|
122
|
+
timeout: params[:timeout] || 600
|
|
123
|
+
)
|
|
124
|
+
if completed[:video_url] && params[:download_to_file]
|
|
125
|
+
output_dir = params[:output_dir] || "./generated_videos"
|
|
126
|
+
prefix = params[:filename_prefix] || "siliconflow_video"
|
|
127
|
+
output_path = File.join(output_dir, "#{prefix}_#{submitted[:request_id]}.mp4")
|
|
128
|
+
downloaded = adapter.download_video(completed[:video_url], output_path)
|
|
129
|
+
result = { submitted: submitted, video: completed, downloaded_file: downloaded }
|
|
130
|
+
else
|
|
131
|
+
result = { submitted: submitted, video: completed }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
result
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# 8. 语音合成 (TTS — CosyVoice2 / MOSS-TTSD). Saves the synthesized audio to disk.
|
|
138
|
+
SmartPrompt.define_worker :siliconflow_tts do
|
|
139
|
+
use "sf_tts"
|
|
140
|
+
model params[:model] if params[:model]
|
|
141
|
+
adapter = engine.llms["sf_tts"]
|
|
142
|
+
|
|
143
|
+
output_path = params[:output_path] || "./generated_audio/siliconflow_tts.mp3"
|
|
144
|
+
info = adapter.synthesize_to_file(
|
|
145
|
+
params[:text],
|
|
146
|
+
output_path,
|
|
147
|
+
voice: params[:voice],
|
|
148
|
+
model: params[:model],
|
|
149
|
+
response_format: params[:response_format] || "mp3",
|
|
150
|
+
speed: params[:speed],
|
|
151
|
+
language: params[:language],
|
|
152
|
+
)
|
|
153
|
+
info
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# 9. 语音识别 (ASR — SenseVoiceSmall). Transcribes a local audio file.
|
|
157
|
+
SmartPrompt.define_worker :siliconflow_asr do
|
|
158
|
+
use "sf_asr"
|
|
159
|
+
model params[:model] if params[:model]
|
|
160
|
+
adapter = engine.llms["sf_asr"]
|
|
161
|
+
|
|
162
|
+
adapter.transcribe_audio(
|
|
163
|
+
params[:audio_file],
|
|
164
|
+
model: params[:model],
|
|
165
|
+
language: params[:language],
|
|
166
|
+
)
|
|
167
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smart_prompt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- zhuang biaowei
|
|
@@ -152,6 +152,7 @@ files:
|
|
|
152
152
|
- config/image_generation_config.yml
|
|
153
153
|
- config/multimodal_config.yml
|
|
154
154
|
- config/sensenova_config.yml
|
|
155
|
+
- config/siliconflow_config.yml
|
|
155
156
|
- config/zhipu_config.yml
|
|
156
157
|
- docs/ANTHROPIC_EXAMPLES.md
|
|
157
158
|
- docs/CONVERSATION_INTEGRATION_SUMMARY.md
|
|
@@ -176,14 +177,31 @@ files:
|
|
|
176
177
|
- examples/multimodal_example.rb
|
|
177
178
|
- examples/relevance_based_strategy_example.rb
|
|
178
179
|
- examples/sensenova_example.rb
|
|
180
|
+
- examples/siliconflow_example.rb
|
|
179
181
|
- examples/stt_example.rb
|
|
180
182
|
- examples/tts_example.rb
|
|
181
183
|
- examples/video_generation_example.rb
|
|
182
184
|
- examples/zhipu_example.rb
|
|
183
185
|
- lib/smart_prompt.rb
|
|
186
|
+
- lib/smart_prompt/adapters/siliconflow/embed.rb
|
|
187
|
+
- lib/smart_prompt/adapters/siliconflow/image.rb
|
|
188
|
+
- lib/smart_prompt/adapters/siliconflow/rerank.rb
|
|
189
|
+
- lib/smart_prompt/adapters/siliconflow/text.rb
|
|
190
|
+
- lib/smart_prompt/adapters/siliconflow/video.rb
|
|
191
|
+
- lib/smart_prompt/adapters/siliconflow/voice.rb
|
|
192
|
+
- lib/smart_prompt/adapters/zhipu/embed.rb
|
|
193
|
+
- lib/smart_prompt/adapters/zhipu/image.rb
|
|
194
|
+
- lib/smart_prompt/adapters/zhipu/rerank.rb
|
|
195
|
+
- lib/smart_prompt/adapters/zhipu/text.rb
|
|
196
|
+
- lib/smart_prompt/adapters/zhipu/video.rb
|
|
197
|
+
- lib/smart_prompt/adapters/zhipu/voice.rb
|
|
184
198
|
- lib/smart_prompt/anthropic_adapter.rb
|
|
185
199
|
- lib/smart_prompt/api_handler.rb
|
|
186
200
|
- lib/smart_prompt/compression_engine.rb
|
|
201
|
+
- lib/smart_prompt/concerns/http_client.rb
|
|
202
|
+
- lib/smart_prompt/concerns/image_persistence.rb
|
|
203
|
+
- lib/smart_prompt/concerns/multimodal_messages.rb
|
|
204
|
+
- lib/smart_prompt/concerns/openai_chat_shaping.rb
|
|
187
205
|
- lib/smart_prompt/context_strategy.rb
|
|
188
206
|
- lib/smart_prompt/conversation.rb
|
|
189
207
|
- lib/smart_prompt/db_adapter.rb
|
|
@@ -202,6 +220,7 @@ files:
|
|
|
202
220
|
- lib/smart_prompt/relevance_based_strategy.rb
|
|
203
221
|
- lib/smart_prompt/sensenova_adapter.rb
|
|
204
222
|
- lib/smart_prompt/session.rb
|
|
223
|
+
- lib/smart_prompt/siliconflow_adapter.rb
|
|
205
224
|
- lib/smart_prompt/sliding_window_strategy.rb
|
|
206
225
|
- lib/smart_prompt/stt_adapter.rb
|
|
207
226
|
- lib/smart_prompt/summary_based_strategy.rb
|
|
@@ -216,6 +235,7 @@ files:
|
|
|
216
235
|
- workers/image_generation_workers.rb
|
|
217
236
|
- workers/multimodal_workers.rb
|
|
218
237
|
- workers/sensenova_workers.rb
|
|
238
|
+
- workers/siliconflow_workers.rb
|
|
219
239
|
- workers/stt_workers.rb
|
|
220
240
|
- workers/tts_workers.rb
|
|
221
241
|
- workers/video_generation_workers.rb
|