diff --git a/Cargo.lock b/Cargo.lock index 5b9b7de0..a232bdfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -478,7 +478,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "c0mpute-api" -version = "0.2.81" +version = "0.2.82" dependencies = [ "anyhow", "c0mpute-proto", @@ -493,7 +493,7 @@ dependencies = [ [[package]] name = "c0mpute-bench" -version = "0.2.81" +version = "0.2.82" dependencies = [ "anyhow", "blake3", @@ -504,7 +504,7 @@ dependencies = [ [[package]] name = "c0mpute-cli" -version = "0.2.81" +version = "0.2.82" dependencies = [ "anyhow", "axum", @@ -534,7 +534,7 @@ dependencies = [ [[package]] name = "c0mpute-core" -version = "0.2.81" +version = "0.2.82" dependencies = [ "aes-gcm", "anyhow", @@ -579,7 +579,7 @@ dependencies = [ [[package]] name = "c0mpute-doctor" -version = "0.2.81" +version = "0.2.82" dependencies = [ "anyhow", "serde", @@ -591,7 +591,7 @@ dependencies = [ [[package]] name = "c0mpute-envelope" -version = "0.2.81" +version = "0.2.82" dependencies = [ "blake3", "bs58", @@ -604,7 +604,7 @@ dependencies = [ [[package]] name = "c0mpute-gateway" -version = "0.2.81" +version = "0.2.82" dependencies = [ "anyhow", "axum", @@ -626,7 +626,7 @@ dependencies = [ [[package]] name = "c0mpute-market" -version = "0.2.81" +version = "0.2.82" dependencies = [ "c0mpute-envelope", "serde", @@ -636,7 +636,7 @@ dependencies = [ [[package]] name = "c0mpute-net" -version = "0.2.81" +version = "0.2.82" dependencies = [ "anyhow", "async-trait", @@ -653,7 +653,7 @@ dependencies = [ [[package]] name = "c0mpute-placement" -version = "0.2.81" +version = "0.2.82" dependencies = [ "anyhow", "async-trait", @@ -674,7 +674,7 @@ dependencies = [ [[package]] name = "c0mpute-proto" -version = "0.2.81" +version = "0.2.82" dependencies = [ "blake3", "hex", @@ -686,7 +686,7 @@ dependencies = [ [[package]] name = "c0mpute-secure-chat" -version = "0.2.81" +version = "0.2.82" dependencies = [ "aes-gcm", "anyhow", @@ -709,7 +709,7 @@ dependencies = [ [[package]] name = "c0mpute-store" -version = "0.2.81" +version = "0.2.82" dependencies = [ "anyhow", "blake3", @@ -727,7 +727,7 @@ dependencies = [ [[package]] name = "c0mpute-transcode" -version = "0.2.81" +version = "0.2.82" dependencies = [ "anyhow", "c0mpute-proto", @@ -740,7 +740,7 @@ dependencies = [ [[package]] name = "c0mpute-update" -version = "0.2.81" +version = "0.2.82" dependencies = [ "anyhow", "hex", @@ -754,7 +754,7 @@ dependencies = [ [[package]] name = "c0mpute-verify" -version = "0.2.81" +version = "0.2.82" dependencies = [ "anyhow", "blake3", diff --git a/Cargo.toml b/Cargo.toml index af39839f..6af6290e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.2.81" +version = "0.2.82" edition = "2024" license = "MIT" repository = "https://github.com/profullstack/c0mpute" diff --git a/apps/tui/package.json b/apps/tui/package.json index 0f787e5d..f8e9df37 100644 --- a/apps/tui/package.json +++ b/apps/tui/package.json @@ -1,6 +1,6 @@ { "name": "@c0mpute/tui", - "version": "0.2.81", + "version": "0.2.82", "private": true, "type": "module", "bin": { diff --git a/apps/web/package.json b/apps/web/package.json index 66b4ec9e..9f23c7cb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "c0mpute", - "version": "0.2.81", + "version": "0.2.82", "private": true, "type": "module", "scripts": { diff --git a/apps/web/src/app/releases/latest.json/route.ts b/apps/web/src/app/releases/latest.json/route.ts index 100b3db6..baac1d39 100644 --- a/apps/web/src/app/releases/latest.json/route.ts +++ b/apps/web/src/app/releases/latest.json/route.ts @@ -28,7 +28,7 @@ export const dynamic = "force-dynamic"; * this in sync with the workspace version in `Cargo.toml` on each release so a * freshly built CLI reports "already latest" rather than a phantom upgrade. */ -const FALLBACK_VERSION = "0.2.81"; +const FALLBACK_VERSION = "0.2.82"; const FALLBACK_MIN_REQUIRED = "0.0.1"; type Channel = "stable" | "beta" | "nightly"; diff --git a/apps/web/src/lib/chat/music.test.ts b/apps/web/src/lib/chat/music.test.ts index a027c01e..e2669191 100644 --- a/apps/web/src/lib/chat/music.test.ts +++ b/apps/web/src/lib/chat/music.test.ts @@ -17,6 +17,8 @@ describe("chat music", () => { expect(typeof parseMusicRequest({ prompt: "x", duration: 5 })).toBe("string"); expect(typeof parseMusicRequest({ prompt: "x", duration: 200 })).toBe("string"); expect(typeof parseMusicRequest({ prompt: "" })).toBe("string"); + expect(parseMusicRequest({ prompt: "x".repeat(100_000), lyrics: "y".repeat(100_000) })).toMatchObject({ duration: 30 }); + expect(typeof parseMusicRequest({ prompt: "x".repeat(100_001) })).toBe("string"); expect(typeof parseMusicRequest(null)).toBe("string"); }); test("file extensions follow the content type", () => { diff --git a/apps/web/src/lib/chat/music.ts b/apps/web/src/lib/chat/music.ts index 364d0e91..2db3be86 100644 --- a/apps/web/src/lib/chat/music.ts +++ b/apps/web/src/lib/chat/music.ts @@ -20,6 +20,7 @@ export function parseMusicCommand(text: string): MusicCommand | null { return prompt ? { prompt, lyrics } : null; } +export const MAX_TEXT_CHARS = 100_000; export const MIN_SECONDS = 10; export const MAX_SECONDS = 120; @@ -29,9 +30,10 @@ export function parseMusicRequest( ): { prompt: string; lyrics: string; duration: number } | string { const b = (body ?? {}) as Record; const prompt = typeof b.prompt === "string" ? b.prompt.trim() : ""; - if (!prompt || prompt.length > 1000) return "prompt must contain 1 to 1000 characters"; + // Long briefs and whole songs are fine; Infernet clips what the model reads. + if (!prompt || prompt.length > MAX_TEXT_CHARS) return `prompt must contain 1 to ${MAX_TEXT_CHARS} characters`; const lyrics = typeof b.lyrics === "string" ? b.lyrics.trim() : ""; - if (lyrics.length > 4000) return "lyrics must be at most 4000 characters"; + if (lyrics.length > MAX_TEXT_CHARS) return `lyrics must be at most ${MAX_TEXT_CHARS} characters`; const duration = b.duration === undefined || b.duration === null ? 30 : Number(b.duration); if (!Number.isInteger(duration) || duration < MIN_SECONDS || duration > MAX_SECONDS) { return `duration must be a whole number of seconds from ${MIN_SECONDS} to ${MAX_SECONDS}`; diff --git a/node/crates/c0mpute-core/src/music.rs b/node/crates/c0mpute-core/src/music.rs index e5a0f8ac..60cb9884 100644 --- a/node/crates/c0mpute-core/src/music.rs +++ b/node/crates/c0mpute-core/src/music.rs @@ -45,8 +45,15 @@ pub const MODEL_TAG: &str = "c0mpute:music:model:ace-step-1.5"; /// Largest song a worker returns or a buyer accepts. A 2-minute 128 kb/s MP3 /// is ~2 MB; the FLAC fallback for the same song is up to ~20 MB. pub const MAX_RESULT_BYTES: usize = 32 * 1024 * 1024; -pub const MAX_PROMPT_CHARS: usize = 1_000; -pub const MAX_LYRICS_CHARS: usize = 4_000; +/// What a buyer may send. Longer than the model reads on purpose: callers +/// paste whole briefs and songs, and `clip_for_model` trims them. +pub const MAX_PROMPT_CHARS: usize = 100_000; +pub const MAX_LYRICS_CHARS: usize = 100_000; +/// What reaches the worker: ACE-Step's tokenizer keeps 256 caption tokens +/// (~1000 chars) and 2048 lyric tokens, and the spec rides in a gossiped +/// offer capped at 64 KiB, so the gateway clips to these before auctioning. +pub const MODEL_PROMPT_CHARS: usize = 1_000; +pub const MODEL_LYRICS_CHARS: usize = 4_000; pub const MIN_SECONDS: u32 = 10; pub const MAX_SECONDS: u32 = 120; pub const DEFAULT_SECONDS: u32 = 30; @@ -398,7 +405,27 @@ fn default_steps() -> u32 { 8 } +/// Clip to `max` characters, preferring to end at a line break or space. +pub fn clip_text(text: &str, max: usize) -> String { + let text = text.trim(); + if text.chars().count() <= max { + return text.to_string(); + } + let head: String = text.chars().take(max).collect(); + let cut = head.rfind(['\n', ' ']).filter(|&i| i * 5 > head.len() * 4); + cut.map_or(head.as_str(), |i| &head[..i]).trim().to_string() +} + impl Txt2MusicSpec { + /// The same request trimmed to what the model reads (see `MODEL_*_CHARS`). + pub fn clip_for_model(&self) -> Self { + Self { + prompt: clip_text(&self.prompt, MODEL_PROMPT_CHARS), + lyrics: clip_text(&self.lyrics, MODEL_LYRICS_CHARS), + ..self.clone() + } + } + /// Shared by the gateway (reject early) and the worker (never trust the buyer). pub fn validate(&self) -> std::result::Result<(), String> { let chars = self.prompt.trim().chars().count(); @@ -676,6 +703,23 @@ mod tests { assert!(s.validate().is_err()); } + #[test] + fn long_text_is_accepted_and_clipped_for_the_model() { + let long = "word ".repeat(19_999) + "word"; + let mut s = spec(); + s.prompt = long.clone(); + s.lyrics = long; + assert!(s.validate().is_ok()); + let c = s.clip_for_model(); + assert!(c.prompt.chars().count() <= MODEL_PROMPT_CHARS && c.prompt.chars().count() > 800); + assert!(c.prompt.ends_with("word")); + assert!(c.lyrics.chars().count() <= MODEL_LYRICS_CHARS); + assert!(serde_json::to_vec(&c).unwrap().len() < 32 * 1024, "fits a gossip offer"); + assert!(c.validate().is_ok()); + assert_eq!(clip_text(" short ", 10), "short"); + assert_eq!(clip_text(&"\u{e9}".repeat(5000), 1000).chars().count(), 1000); + } + #[test] fn audio_types_come_from_magic_bytes() { let pad = |head: &[u8]| { diff --git a/node/crates/c0mpute-core/src/speech_gateway.rs b/node/crates/c0mpute-core/src/speech_gateway.rs index 6095f688..a26ef1ab 100644 --- a/node/crates/c0mpute-core/src/speech_gateway.rs +++ b/node/crates/c0mpute-core/src/speech_gateway.rs @@ -35,7 +35,9 @@ const IMAGE_QUEUE_WAIT: Duration = Duration::from_secs(4 * 60); /// Songs in flight through one gateway; each holds a whole worker for minutes. const MAX_CONCURRENT_MUSIC_JOBS: usize = 4; /// How long to keep re-auctioning a song while every music worker is busy. -const MUSIC_QUEUE_WAIT: Duration = Duration::from_secs(3 * 60); +/// A song holds a worker ~2.5 min on CPU, so this waits behind two or three; +/// Infernet gives the whole call 19 min. +const MUSIC_QUEUE_WAIT: Duration = Duration::from_secs(8 * 60); const MAX_CONCURRENT_OCR_JOBS: usize = 4; const MAX_CONCURRENT_EMBED_JOBS: usize = 8; @@ -1000,7 +1002,8 @@ async fn generate_music( .acquire_owned() .await .map_err(|_| ApiError(StatusCode::SERVICE_UNAVAILABLE, "music queue is unavailable".into()))?; - let spec = serde_json::to_value(&input).map_err(|error| gateway_failure(error.into()))?; + let spec = serde_json::to_value(input.clip_for_model()) + .map_err(|error| gateway_failure(error.into()))?; let (outcome, bytes) = run_speech_job( &state, crate::music::WORKLOAD_TXT2MUSIC, diff --git a/package.json b/package.json index 0c664975..ffb6bb59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "c0mpute-monorepo", - "version": "0.2.81", + "version": "0.2.82", "private": true, "workspaces": [ "apps/*", diff --git a/packages/shared/package.json b/packages/shared/package.json index 9e74d2c4..14d584bf 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@c0mpute/shared", - "version": "0.2.81", + "version": "0.2.82", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/plugins/ad-generator-agent/module.toml b/plugins/ad-generator-agent/module.toml index 968f6d48..6333c736 100644 --- a/plugins/ad-generator-agent/module.toml +++ b/plugins/ad-generator-agent/module.toml @@ -21,7 +21,7 @@ [module] id = "ad-generator-agent" name = "Ad Generator Agent — every ad type from one URL" -version = "0.2.81" +version = "0.2.82" kind = "service" category = "applications" status = "available" diff --git a/plugins/ads-manager/module.toml b/plugins/ads-manager/module.toml index ade7b096..0444e7b8 100644 --- a/plugins/ads-manager/module.toml +++ b/plugins/ads-manager/module.toml @@ -38,7 +38,7 @@ [module] id = "ads-manager" name = "Ads Manager — shared ad operations layer" -version = "0.2.81" +version = "0.2.82" kind = "service" category = "applications" status = "partial" diff --git a/plugins/coinpay/module.toml b/plugins/coinpay/module.toml index b3662868..b9e47b06 100644 --- a/plugins/coinpay/module.toml +++ b/plugins/coinpay/module.toml @@ -7,7 +7,7 @@ [module] id = "coinpay" name = "CoinPay — DID, payments, escrow, reputation" -version = "0.2.81" +version = "0.2.82" kind = "service" category = "applications" status = "available" diff --git a/plugins/crawl/module.toml b/plugins/crawl/module.toml index e45c7d10..a39bb91f 100644 --- a/plugins/crawl/module.toml +++ b/plugins/crawl/module.toml @@ -8,7 +8,7 @@ [module] id = "crawl" name = "Crawl — distributed web fetching" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "data" status = "available" diff --git a/plugins/ctv-ads/module.toml b/plugins/ctv-ads/module.toml index e338e423..3a63cad1 100644 --- a/plugins/ctv-ads/module.toml +++ b/plugins/ctv-ads/module.toml @@ -28,7 +28,7 @@ [module] id = "ctv-ads" name = "CTV Ads — VAST and VMAP from every gateway" -version = "0.2.81" +version = "0.2.82" kind = "service" category = "applications" status = "partial" diff --git a/plugins/diffusion/module.toml b/plugins/diffusion/module.toml index 26d0c0ff..79c368a8 100644 --- a/plugins/diffusion/module.toml +++ b/plugins/diffusion/module.toml @@ -11,7 +11,7 @@ [module] id = "diffusion" name = "Diffusion — image generation" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "ai" status = "available" diff --git a/plugins/embed/module.toml b/plugins/embed/module.toml index a13249bf..818f6fa5 100644 --- a/plugins/embed/module.toml +++ b/plugins/embed/module.toml @@ -19,7 +19,7 @@ [module] id = "embed" name = "Embeddings — vector generation" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "ai" status = "available" diff --git a/plugins/gameserver/module.toml b/plugins/gameserver/module.toml index 57a363d3..f349e31a 100644 --- a/plugins/gameserver/module.toml +++ b/plugins/gameserver/module.toml @@ -55,7 +55,7 @@ [module] id = "gameserver" name = "Game server — Minecraft / Factorio / Valheim" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "infrastructure" status = "available" diff --git a/plugins/hosting/module.toml b/plugins/hosting/module.toml index 4e5063f9..8a5b4ca8 100644 --- a/plugins/hosting/module.toml +++ b/plugins/hosting/module.toml @@ -30,7 +30,7 @@ [module] id = "hosting" name = "Hosting — p2p static sites" -version = "0.2.81" +version = "0.2.82" kind = "service" category = "data" status = "available" diff --git a/plugins/infernet/module.toml b/plugins/infernet/module.toml index 91a0db19..ee5e3eed 100644 --- a/plugins/infernet/module.toml +++ b/plugins/infernet/module.toml @@ -22,7 +22,7 @@ [module] id = "infernet" name = "Infernet Protocol — AI inference" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "ai" status = "available" diff --git a/plugins/live-stream/module.toml b/plugins/live-stream/module.toml index 22b10f86..2ea55908 100644 --- a/plugins/live-stream/module.toml +++ b/plugins/live-stream/module.toml @@ -34,7 +34,7 @@ [module] id = "live-stream" name = "Live Stream — p2p HLS live streaming" -version = "0.2.81" +version = "0.2.82" kind = "service" category = "media" status = "available" diff --git a/plugins/music/module.toml b/plugins/music/module.toml index 479f01f1..246a5c1e 100644 --- a/plugins/music/module.toml +++ b/plugins/music/module.toml @@ -15,7 +15,7 @@ [module] id = "music" name = "Music — song generation (ACE-Step 1.5)" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "ai" status = "available" diff --git a/plugins/ocr/module.toml b/plugins/ocr/module.toml index d33b44c3..a5a8a0b2 100644 --- a/plugins/ocr/module.toml +++ b/plugins/ocr/module.toml @@ -19,7 +19,7 @@ [module] id = "ocr" name = "OCR — document text extraction" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "ai" status = "available" diff --git a/plugins/podcast-ads/module.toml b/plugins/podcast-ads/module.toml index 92787e6a..e8958c2e 100644 --- a/plugins/podcast-ads/module.toml +++ b/plugins/podcast-ads/module.toml @@ -31,7 +31,7 @@ [module] id = "podcast-ads" name = "Podcast Ads — dynamic ad insertion" -version = "0.2.81" +version = "0.2.82" kind = "service" category = "applications" status = "partial" diff --git a/plugins/podcasting/module.toml b/plugins/podcasting/module.toml index fb3b3822..08539a87 100644 --- a/plugins/podcasting/module.toml +++ b/plugins/podcasting/module.toml @@ -24,7 +24,7 @@ [module] id = "podcasting" name = "Podcasting — p2p podcast publishing" -version = "0.2.81" +version = "0.2.82" kind = "service" category = "media" status = "available" diff --git a/plugins/podcasts/module.toml b/plugins/podcasts/module.toml index 2c9c7882..5c4e67c6 100644 --- a/plugins/podcasts/module.toml +++ b/plugins/podcasts/module.toml @@ -24,7 +24,7 @@ [module] id = "podcasts" name = "Podcasts — p2p podcast listener" -version = "0.2.81" +version = "0.2.82" kind = "service" category = "media" status = "available" diff --git a/plugins/render/module.toml b/plugins/render/module.toml index 47ed9919..cafbc866 100644 --- a/plugins/render/module.toml +++ b/plugins/render/module.toml @@ -33,7 +33,7 @@ [module] id = "render" name = "Render — Blender 3D rendering" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "media" status = "available" diff --git a/plugins/secure-chat/module.toml b/plugins/secure-chat/module.toml index 68a78ca6..0cfcdbfd 100644 --- a/plugins/secure-chat/module.toml +++ b/plugins/secure-chat/module.toml @@ -18,7 +18,7 @@ [module] id = "secure-chat" name = "Secure Chat — E2E encrypted p2p messaging" -version = "0.2.81" +version = "0.2.82" kind = "service" category = "applications" status = "partial" diff --git a/plugins/storage/module.toml b/plugins/storage/module.toml index 9045feb9..f0a8f011 100644 --- a/plugins/storage/module.toml +++ b/plugins/storage/module.toml @@ -20,7 +20,7 @@ [module] id = "storage" name = "Storage — Reed-Solomon shard service" -version = "0.2.81" +version = "0.2.82" kind = "service" category = "data" status = "available" diff --git a/plugins/train/module.toml b/plugins/train/module.toml index 15c7e0ce..693f96fe 100644 --- a/plugins/train/module.toml +++ b/plugins/train/module.toml @@ -41,7 +41,7 @@ [module] id = "train" name = "Fine-tune — LoRA training" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "ai" status = "available" diff --git a/plugins/transcode/module.toml b/plugins/transcode/module.toml index ce5868e1..cd2d38b4 100644 --- a/plugins/transcode/module.toml +++ b/plugins/transcode/module.toml @@ -7,7 +7,7 @@ [module] id = "transcode" name = "FFmpeg Transcoding" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "media" status = "available" diff --git a/plugins/tts/module.toml b/plugins/tts/module.toml index e694c910..7aff9f49 100644 --- a/plugins/tts/module.toml +++ b/plugins/tts/module.toml @@ -15,7 +15,7 @@ [module] id = "tts" name = "TTS — text-to-speech" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "ai" status = "available" diff --git a/plugins/video/module.toml b/plugins/video/module.toml index 636357b1..a7119386 100644 --- a/plugins/video/module.toml +++ b/plugins/video/module.toml @@ -50,7 +50,7 @@ [module] id = "video" name = "Video — generation (Wan 2.2, LTX-2.3 with audio)" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "ai" status = "available" diff --git a/plugins/webhook/module.toml b/plugins/webhook/module.toml index 945cb80b..6f831c22 100644 --- a/plugins/webhook/module.toml +++ b/plugins/webhook/module.toml @@ -7,7 +7,7 @@ [module] id = "webhook" name = "Webhooks — paid, end-to-end encrypted delivery" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "infrastructure" status = "available" diff --git a/plugins/whisper/module.toml b/plugins/whisper/module.toml index af4c15fe..3498ed63 100644 --- a/plugins/whisper/module.toml +++ b/plugins/whisper/module.toml @@ -7,7 +7,7 @@ [module] id = "whisper" name = "Whisper — speech-to-text" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "ai" status = "available" diff --git a/plugins/workflow-step/module.toml b/plugins/workflow-step/module.toml index 98095cf6..7f842e83 100644 --- a/plugins/workflow-step/module.toml +++ b/plugins/workflow-step/module.toml @@ -14,7 +14,7 @@ [module] id = "workflow-step" name = "Workflow step — webhook workflow steps (meshhook)" -version = "0.2.81" +version = "0.2.82" kind = "workload" category = "infrastructure" status = "available"