Commit 15ff261e24b07f4c3adb00c2c6aa6eb1e6e3e6f5

Authored by yibo
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 || &quot;&quot;; @@ -19,6 +19,15 @@ const H3_CONSOLE_BASE = process.env.H3_CONSOLE_BASE || &quot;&quot;;
19 const H3_CONSOLE_TOKEN = process.env.H3_CONSOLE_TOKEN || ""; 19 const H3_CONSOLE_TOKEN = process.env.H3_CONSOLE_TOKEN || "";
20 const H3_CONSOLE_URL = H3_CONSOLE_BASE ? new URL(H3_CONSOLE_BASE) : null; 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 const MIME = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".json": "application/json" }; 31 const MIME = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".json": "application/json" };
23 32
24 const { syncLibrary } = require("./ingest.js"); 33 const { syncLibrary } = require("./ingest.js");
@@ -91,6 +100,58 @@ function h3ConsoleFetch(pathAndQuery, options) { @@ -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 // ---- Live channel: shared virtual playhead + prompt queue ---- 155 // ---- Live channel: shared virtual playhead + prompt queue ----
95 // Persisted to live-state.json (see saveLiveState/loadLiveState) so a 156 // Persisted to live-state.json (see saveLiveState/loadLiveState) so a
96 // restart resumes the schedule instead of starting cold; ensureSchedule's 157 // restart resumes the schedule instead of starting cold; ensureSchedule's
@@ -277,9 +338,13 @@ function isRateLimited(ip) { @@ -277,9 +338,13 @@ function isRateLimited(ip) {
277 async function generateForPrompt(entry) { 338 async function generateForPrompt(entry) {
278 try { 339 try {
279 if (!entry.h3JobId) { 340 if (!entry.h3JobId) {
  341 + if (!entry.expandedPrompt) {
  342 + entry.expandedPrompt = await optimizePrompt(entry.prompt);
  343 + saveLiveState();
  344 + }
280 const submitted = await h3ConsoleFetch("/v1/videos", { 345 const submitted = await h3ConsoleFetch("/v1/videos", {
281 method: "POST", 346 method: "POST",
282 - body: { prompt: entry.prompt, ...H3_GEN_PARAMS }, 347 + body: { prompt: entry.expandedPrompt, ...H3_GEN_PARAMS },
283 }); 348 });
284 if (!submitted.id) throw new Error(submitted.detail || "submit failed"); 349 if (!submitted.id) throw new Error(submitted.detail || "submit failed");
285 entry.h3JobId = submitted.id; 350 entry.h3JobId = submitted.id;
@@ -295,7 +360,7 @@ async function generateForPrompt(entry) { @@ -295,7 +360,7 @@ async function generateForPrompt(entry) {
295 id: job.id, 360 id: job.id,
296 source: "h3console", 361 source: "h3console",
297 prompt: entry.prompt, 362 prompt: entry.prompt,
298 - fullPrompt: entry.prompt, 363 + fullPrompt: entry.expandedPrompt || entry.prompt,
299 width: job.req.width, 364 width: job.req.width,
300 height: job.req.height, 365 height: job.req.height,
301 duration: Math.round((job.req.frames / FPS) * 10) / 10, 366 duration: Math.round((job.req.frames / FPS) * 10) / 10,