Commit 15ff261e24b07f4c3adb00c2c6aa6eb1e6e3e6f5
1 parent
1dc70844
Expand chat prompts with GLM before submitting to H3
Adds a prompt-optimization step ahead of H3 generation: a viewer's short
chat line ("一只猫跳舞") gets expanded into a detailed shot-by-shot English
prompt (camera work, lighting, action, setting) via one GLM Coding Plan call
(glm-5.3-flash, /api/coding/ endpoint — the coding-plan-specific path, not
the pay-per-token one) before being handed to H3. The expanded prompt becomes
the clip's fullPrompt; the original short text stays as the display prompt.
Persisted on the queue entry so a restart mid-generation doesn't re-call GLM.
Best-effort throughout — no API key, a timeout, or any GLM error just falls
back to using the original text unchanged, never blocking generation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0144MiJ2cLjMd9jWaeEpVfUJ
Showing
1 changed file
with
67 additions
and
2 deletions
server.js
| ... | ... | @@ -19,6 +19,15 @@ const H3_CONSOLE_BASE = process.env.H3_CONSOLE_BASE || ""; |
| 19 | 19 | const H3_CONSOLE_TOKEN = process.env.H3_CONSOLE_TOKEN || ""; |
| 20 | 20 | const H3_CONSOLE_URL = H3_CONSOLE_BASE ? new URL(H3_CONSOLE_BASE) : null; |
| 21 | 21 | |
| 22 | +// GLM Coding Plan — used only to expand a viewer's short chat prompt into a | |
| 23 | +// detailed shot-by-shot prompt before handing it to H3 (see optimizePrompt). | |
| 24 | +// Must hit the /api/coding/ path specifically — the plain /api/paas/ path | |
| 25 | +// bills against a different (pay-per-token) quota than the coding plan. | |
| 26 | +const GLM_API_KEY = process.env.GLM_API_KEY || ""; | |
| 27 | +const GLM_HOST = "open.bigmodel.cn"; | |
| 28 | +const GLM_PATH = "/api/coding/paas/v4/chat/completions"; | |
| 29 | +const GLM_MODEL = "glm-5.3-flash"; | |
| 30 | + | |
| 22 | 31 | const MIME = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".json": "application/json" }; |
| 23 | 32 | |
| 24 | 33 | const { syncLibrary } = require("./ingest.js"); |
| ... | ... | @@ -91,6 +100,58 @@ function h3ConsoleFetch(pathAndQuery, options) { |
| 91 | 100 | }); |
| 92 | 101 | } |
| 93 | 102 | |
| 103 | +function glmChat(messages) { | |
| 104 | + return new Promise((resolve, reject) => { | |
| 105 | + const body = JSON.stringify({ model: GLM_MODEL, messages, temperature: 0.7 }); | |
| 106 | + const req = https.request( | |
| 107 | + { | |
| 108 | + hostname: GLM_HOST, | |
| 109 | + path: GLM_PATH, | |
| 110 | + method: "POST", | |
| 111 | + headers: { Authorization: `Bearer ${GLM_API_KEY}`, "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }, | |
| 112 | + }, | |
| 113 | + (res) => { | |
| 114 | + let data = ""; | |
| 115 | + res.on("data", (c) => (data += c)); | |
| 116 | + res.on("end", () => { | |
| 117 | + try { | |
| 118 | + resolve(JSON.parse(data)); | |
| 119 | + } catch (err) { | |
| 120 | + reject(err); | |
| 121 | + } | |
| 122 | + }); | |
| 123 | + } | |
| 124 | + ); | |
| 125 | + req.on("error", reject); | |
| 126 | + req.setTimeout(15000, () => req.destroy(new Error("GLM request timed out"))); | |
| 127 | + req.write(body); | |
| 128 | + req.end(); | |
| 129 | + }); | |
| 130 | +} | |
| 131 | + | |
| 132 | +const PROMPT_OPTIMIZE_SYSTEM = `You are a prompt engineer for a text-to-video model. Expand the user's short | |
| 133 | +request (which may be in Chinese or English) into a detailed shot-by-shot generation prompt written in | |
| 134 | +English: camera angle/movement, lighting and mood, the subject's specific actions, and the setting. | |
| 135 | +Output only the expanded English prompt itself — no preamble, no quotes, no "Prompt:" prefix — under | |
| 136 | +150 words.`; | |
| 137 | + | |
| 138 | +// Best-effort — a viewer's short chat line ("一只猫跳舞") makes for a weak | |
| 139 | +// H3 prompt on its own. Never let a GLM outage or slow response block | |
| 140 | +// generation: on any failure this just falls back to the original text. | |
| 141 | +async function optimizePrompt(userPrompt) { | |
| 142 | + if (!GLM_API_KEY) return userPrompt; | |
| 143 | + try { | |
| 144 | + const res = await glmChat([ | |
| 145 | + { role: "system", content: PROMPT_OPTIMIZE_SYSTEM }, | |
| 146 | + { role: "user", content: userPrompt }, | |
| 147 | + ]); | |
| 148 | + const text = res.choices && res.choices[0] && res.choices[0].message && res.choices[0].message.content; | |
| 149 | + return text ? text.trim() : userPrompt; | |
| 150 | + } catch { | |
| 151 | + return userPrompt; | |
| 152 | + } | |
| 153 | +} | |
| 154 | + | |
| 94 | 155 | // ---- Live channel: shared virtual playhead + prompt queue ---- |
| 95 | 156 | // Persisted to live-state.json (see saveLiveState/loadLiveState) so a |
| 96 | 157 | // restart resumes the schedule instead of starting cold; ensureSchedule's |
| ... | ... | @@ -277,9 +338,13 @@ function isRateLimited(ip) { |
| 277 | 338 | async function generateForPrompt(entry) { |
| 278 | 339 | try { |
| 279 | 340 | if (!entry.h3JobId) { |
| 341 | + if (!entry.expandedPrompt) { | |
| 342 | + entry.expandedPrompt = await optimizePrompt(entry.prompt); | |
| 343 | + saveLiveState(); | |
| 344 | + } | |
| 280 | 345 | const submitted = await h3ConsoleFetch("/v1/videos", { |
| 281 | 346 | method: "POST", |
| 282 | - body: { prompt: entry.prompt, ...H3_GEN_PARAMS }, | |
| 347 | + body: { prompt: entry.expandedPrompt, ...H3_GEN_PARAMS }, | |
| 283 | 348 | }); |
| 284 | 349 | if (!submitted.id) throw new Error(submitted.detail || "submit failed"); |
| 285 | 350 | entry.h3JobId = submitted.id; |
| ... | ... | @@ -295,7 +360,7 @@ async function generateForPrompt(entry) { |
| 295 | 360 | id: job.id, |
| 296 | 361 | source: "h3console", |
| 297 | 362 | prompt: entry.prompt, |
| 298 | - fullPrompt: entry.prompt, | |
| 363 | + fullPrompt: entry.expandedPrompt || entry.prompt, | |
| 299 | 364 | width: job.req.width, |
| 300 | 365 | height: job.req.height, |
| 301 | 366 | duration: Math.round((job.req.frames / FPS) * 10) / 10, | ... | ... |