Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 15 additions & 20 deletions app/_launch_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -27574,7 +27574,7 @@ def _story_stage_problem(result: dict, scope: str, project: dict) -> str | None:
def _story_project_prompt_context(project: dict, scope: str) -> str:
"""Return bounded, valid JSON with editorial facts but no heavy runtime data."""
overview_keys = (
"title", "projectType", "creativeBrief", "language", "spokenLanguage", "locationVariety",
"title", "projectType", "creativeBrief", "language", "spokenLanguage", "languageIntent", "locationVariety",
Comment thread
cursor[bot] marked this conversation as resolved.
"protagonistConsistency", "protagonistCharacterId", "genre", "tone", "audience", "premise",
"logline", "synopsis", "theme", "ending", "visualStyle",
"characterVisualStyle", "enforceVisualStyle", "allowClipText",
Expand Down Expand Up @@ -27802,7 +27802,7 @@ def get_series_project_endpoint(series_id: str, workspace: str | None = None):

@api.put("/api/v1/series/{series_id}")
def put_series_project_endpoint(series_id: str, body: dict):
from services.series_library import normalize_series_project
from services.series_library import normalize_series_project, series_canon_inputs_changed

workspace = _series_library_workspace(body.get("workspace"))
raw_series = body.get("series")
Expand All @@ -27821,19 +27821,7 @@ def put_series_project_endpoint(series_id: str, body: dict):
detail=f"Series revision changed to {current.get('revision')}; reload before saving",
)
updated = normalize_series_project({**raw_series, "id": series_id}, series_id, workspace)
canon_inputs = (
"title", "premise", "logline", "format", "language", "spokenLanguage",
"protagonistConsistency", "protagonistCharacterId", "genre", "tone", "audience",
"visualStyle", "characterVisualStyle", "cameraLanguage", "sourceMode",
"masterUniversePrompt", "characters", "relationships", "locations", "props",
)
current_canon = copy.deepcopy(current.get("canon") or {})
updated_canon = copy.deepcopy(updated.get("canon") or {})
for value in (current_canon, updated_canon):
value.pop("approval", None); value.pop("approvedAt", None)
if current_canon != updated_canon or any(
current.get(key) != updated.get(key) for key in canon_inputs
):
if series_canon_inputs_changed(current, updated):
updated["canon"]["approval"] = "draft"
updated["canon"]["approvedAt"] = ""
updated["revision"] = int(current.get("revision") or 1) + 1
Expand Down Expand Up @@ -30427,8 +30415,9 @@ async def analyze_story_assets(body: dict):
images of the same new entity, give all of them the same stable grouping key:
"new-character:<slug>" or "new-location:<slug>". World/prop/style use targetId
"world". Describe only visible evidence; do not invent biography or plot facts.
Write name, description, visualPrompt and reason in {language}. visualPrompt is
a reusable single-image identity/environment reference prompt without grids,
Write reader-facing name, description and reason in {language}. Write visualPrompt in
English because it is provider-facing technical direction. visualPrompt is a reusable
single-image identity/environment reference prompt without grids,
collages, captions, logos or UI. Confidence is 0 to 1. Return strict JSON only."""
schema = asset_import_schema(len(paths))
override = _comic_writing_llm(body)
Expand Down Expand Up @@ -30526,7 +30515,7 @@ def _generate_story_lab_stage(body: dict, scope: str) -> dict:
- Target duration: {max(20, min(360, brief_duration or 90))} seconds.
- referenceSong is an editable inspiration example in "Title — Artist" form. Use it only
for broad tempo, instrumentation or emotional architecture; never copy melody or lyrics.
- style is the final MiniMax Music prompt: one concise {language} comma-separated line,
- style is the final MiniMax Music prompt: one concise English comma-separated line,
10–300 characters, covering genre, mood, instruments, vocals, tempo and production.
- Write lyrics in {language}, maximum 3500 characters, with a recurring hook and a clear
narrative progression. Use supported English tags on their own lines: [Intro], [Verse],
Expand All @@ -30545,7 +30534,7 @@ def _generate_story_lab_stage(body: dict, scope: str) -> dict:
reproduce the reference song's melody, lyrics, title phrases or distinctive arrangement.
- Treat referenceSong, brief, the Story canon and requested lyric theme as INPUTS to transform.
The final style field must never contain the reference title or artist name.
- style is the final MiniMax Music prompt. Write one concise {language} comma-separated line,
- style is the final MiniMax Music prompt. Write one concise English comma-separated line,
10–300 characters, ordered as applicable: primary genre/subgenre, secondary influence,
mood/atmosphere, key instruments, vocal direction, tempo or BPM, dynamics, production.
Prefer concrete compatible traits; avoid contradictions, filler and narrative synopsis.
Expand Down Expand Up @@ -30609,7 +30598,13 @@ def _generate_story_lab_stage(body: dict, scope: str) -> dict:
base_prompt = f"""Create the requested editable Story Lab material.
Generation scope: {scope}
Premise: {premise}
Language for every reader-facing field: {language}
Language for reader-facing narrative fields: {language}
Technical prompt language: English. Write visualStyle, characterVisualStyle,
visualLanguage, visualPrompt, negativePrompt and every other provider-facing camera,
rendering or production direction in English. Keep exact dialogue, lyrics, subtitles,
visible text and names in the language declared by languageIntent and preserve them
character-for-character; when a technical prompt needs one, isolate it as literal data
and never translate or paraphrase it.
Genre: {genre}
Tone: {tone}
Audience: {audience}
Expand Down
25 changes: 14 additions & 11 deletions app/routers/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@
_SONG_WRITER_FALLBACK = (
"You are a songwriter for ACE-Step 1.5. From the user's brief, output EXACTLY "
"two sections and nothing else:\n[STYLE]\nA dense prose paragraph describing "
"genre, instruments, mood, production, and vocal type (no numeric BPM/key).\n"
"genre, instruments, mood, production, and vocal type in English (no numeric BPM/key).\n"
"[LYRICS]\nOriginal lyrics with [Verse]/[Chorus]/[Bridge] section tags on their "
"own lines, ~6-10 syllables per line. Keep STYLE and LYRICS consistent."
)
_SONG_WRITER_FALLBACK_INSTRUMENTAL = (
"You are a music producer for ACE-Step 1.5. Output EXACTLY two sections:\n"
"[STYLE]\nA dense prose paragraph describing genre, instruments, mood, "
"production, and energy — instrumental, no vocals, no numeric BPM/key.\n"
"production, and energy in English — instrumental, no vocals, no numeric BPM/key.\n"
"[LYRICS]\n[Instrumental]"
)
_SONG_WRITER_FALLBACK_MINIMAX = (
"You write prompts for MiniMax Music. Output exactly [STYLE] and [LYRICS]. "
"Write both STYLE and LYRICS in the language requested by the user. STYLE is one "
"Write STYLE in English and LYRICS in the language requested by the user. STYLE is one "
"comma-separated line of 10-300 characters containing "
"genre, mood, instruments, vocal direction, tempo and production. Never put "
"reference song or artist names in STYLE. LYRICS use supported tags such as "
Expand Down Expand Up @@ -93,9 +93,11 @@ def _minimax_song_request_prompt(body: dict, description: str, instrumental: boo
sections = [
f"MODE: {'instrumental' if instrumental else 'vocal song'}",
f"TARGET MODEL: {model}",
f"STYLE AND LYRICS LANGUAGE: {language}",
f"LANGUAGE RULE: Write the visible STYLE prompt and all sung words in {language}. "
"Keep only provider structural tags such as [Verse] and [Chorus] in English.",
"STYLE LANGUAGE: English (provider-facing technical direction).",
f"LYRICS LANGUAGE: {language}.",
f"LANGUAGE RULE: Write STYLE only in English and all sung words in {language}. "
"Keep provider structural tags such as [Verse] and [Chorus] in English. Preserve "
"every protected exact segment character-for-character and never translate it.",
f"TARGET DURATION: approximately {duration} seconds",
"DURATION NOTE: MiniMax Music has no exact duration API parameter. Treat the target "
"as a strict lyric and arrangement budget: keep the section count and sung lines "
Expand Down Expand Up @@ -134,16 +136,17 @@ def _normalize_minimax_song_output(style: str, lyrics: str, instrumental: bool,


def _ace_song_request_prompt(description: str, language: str, instrumental: bool) -> str:
"""Keep the editable ACE-Step prompt in the language selected by the user."""
"""Keep technical direction in English and lyrics in the selected language."""
target = str(language or "English").strip()[:80] or "English"
if instrumental:
rule = f"Write the visible STYLE prompt in {target}."
rule = "Write the visible provider-facing STYLE prompt in English."
else:
rule = (
f"Write both the visible STYLE prompt and all lyrics in {target}; "
"keep structural tags such as [Verse] and [Chorus] in English."
f"Write the visible provider-facing STYLE prompt in English and all lyrics in {target}; "
"keep structural tags such as [Verse] and [Chorus] in English. Preserve protected "
"exact segments character-for-character."
)
return f"OUTPUT LANGUAGE: {target}. {rule}\n\n{str(description or '').strip()}"
return f"LYRICS LANGUAGE: {target}. TECHNICAL PROMPT LANGUAGE: English. {rule}\n\n{str(description or '').strip()}"


def _song_writer_image_paths(body: dict) -> list:
Expand Down
63 changes: 63 additions & 0 deletions app/services/language_intent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Canonical language intent for durable creative projects.

UI locale is deliberately absent: it is presentation state and cannot choose
the language of authored content or provider prompts.
"""

from __future__ import annotations

from typing import Any


VERBATIM_KINDS = {"dialogue", "lyrics", "visible_text", "subtitle", "name"}


def _text(value: Any, limit: int = 120) -> str:
return str(value or "").strip()[:limit]


def normalize_language_intent(
value: Any,
*,
content_language: str = "",
spoken_language: str = "",
) -> dict[str, Any]:
"""Accept LLM snake_case or persisted camelCase and return canonical JSON."""
raw = value if isinstance(value, dict) else {}
raw_segments = raw.get("verbatimSegments", raw.get("verbatim_segments", []))
segments: list[dict[str, str]] = []
for candidate in raw_segments if isinstance(raw_segments, list) else []:
if not isinstance(candidate, dict):
continue
kind = _text(candidate.get("kind"), 40)
literal = str(candidate.get("text") or "")[:12_000]
if kind not in VERBATIM_KINDS or not literal.strip():
continue
segment = {
"kind": kind,
"text": literal,
"language": _text(candidate.get("language")),
}
speaker = _text(candidate.get("speaker"), 300)
if speaker:
segment["speaker"] = speaker
segments.append(segment)
if len(segments) >= 40:
break
technical = _text(
raw.get("technicalPromptLanguage", raw.get("technical_prompt_language", "en")),
20,
)
return {
"conversationLanguage": _text(
raw.get("conversationLanguage", raw.get("conversation_language"))
),
"contentLanguage": _text(
raw.get("contentLanguage", raw.get("content_language", content_language))
),
"spokenLanguage": _text(
raw.get("spokenLanguage", raw.get("spoken_language", spoken_language))
),
"technicalPromptLanguage": "auto" if technical == "auto" else "en",
"verbatimSegments": segments,
}
2 changes: 1 addition & 1 deletion app/services/llm_guides/music/song_writer.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ You are a professional songwriter writing for ACE-Step 1.5, an AI music model th

Output EXACTLY these two sections and nothing else — no preamble, no explanation:

Write both the visible STYLE prompt and the LYRICS in the output language requested by the user. Keep structural tags such as [Verse] and [Chorus] in English because the music provider parses them.
Write the visible provider-facing STYLE prompt in English. Write the LYRICS in the output language requested by the user. Keep structural tags such as [Verse] and [Chorus] in English because the music provider parses them. Any protected exact segment in the request must remain character-for-character unchanged; never translate or paraphrase it.

[STYLE]
A single dense PROSE paragraph (2-4 sentences) describing the music as if briefing session musicians. Weave together, in natural prose (NOT a bare tag list): genre/subgenre, the key instruments, the overall mood/atmosphere, the production style (e.g. lo-fi, polished, live, bedroom-pop, orchestral), and the vocal type — gender plus texture (breathy, raspy, powerful, falsetto, airy, choir...). Convey tempo/energy in WORDS (slow / mid-tempo / driving / up-tempo). Prefer concrete, specific nouns over vague adjectives; you MAY reference an era or artist style (e.g. "80s synthwave", "Motown soul"). Do NOT write numeric BPM, key, or time signature. Keep it tight; don't stack endless adjectives. If a reference image is attached, let its setting, mood, color, and era guide the STYLE (e.g. neon night city → synthwave; candlelit room → intimate acoustic ballad) — infer the vibe, do NOT describe the image literally.
Expand Down
2 changes: 1 addition & 1 deletion app/services/llm_guides/music/song_writer_instrumental.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ You are a professional music producer designing an INSTRUMENTAL track (no vocals

Output EXACTLY these two sections and nothing else:

Write the visible STYLE prompt in the output language requested by the user.
Write the visible provider-facing STYLE prompt in English, independently of the user's language.

[STYLE]
A single dense PROSE paragraph (2-4 sentences) describing the music as if briefing session musicians: genre/subgenre, the key instruments and how they evolve through the track, the mood/atmosphere, the production style, and the energy/tempo feel (in WORDS, not numbers). This is INSTRUMENTAL — no vocals at all. Prefer concrete, specific nouns over vague adjectives; you may reference an era or style. Do NOT write numeric BPM, key, or time signature. Keep it tight. If a reference image is attached, let its setting, mood, color, and era guide the STYLE (e.g. neon night city → synthwave; misty forest → ambient cinematic) — infer the vibe, do NOT describe the image literally.
Expand Down
4 changes: 3 additions & 1 deletion app/services/llm_guides/music/song_writer_minimax.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ Transform those inputs into a new, self-contained MiniMax-ready style prompt and
Output EXACTLY these two sections and nothing else:

[STYLE]
One concise line in the requested language, 10–300 characters. Use a coherent comma-separated creative brief in this order where relevant: primary genre/subgenre, secondary influence, mood/atmosphere, key instruments, vocal direction, tempo or numeric BPM, dynamics, production character. Be concrete and avoid contradictions, filler, artist names, song names, story synopsis, camera language, and complete sentences. For a cover, describe the new target style rather than the source recording.
One concise line in English, 10–300 characters. This is provider-facing technical direction even when the user and lyrics use another language. Use a coherent comma-separated creative brief in this order where relevant: primary genre/subgenre, secondary influence, mood/atmosphere, key instruments, vocal direction, tempo or numeric BPM, dynamics, production character. Be concrete and avoid contradictions, filler, artist names, song names, story synopsis, camera language, and complete sentences. For a cover, describe the new target style rather than the source recording.

[LYRICS]
For a vocal song, write complete original lyrics in the requested language. Use only these exact supported structural tags, each on its own line: [Intro], [Verse], [Pre Chorus], [Chorus], [Post Chorus], [Interlude], [Bridge], [Transition], [Build Up], [Break], [Hook], [Inst], [Solo], [Outro]. Put a blank line after each tag. Use natural, singable lines, usually 4–8 words each. Build a clear narrative progression, a memorable recurring chorus or hook, and emotional consistency with STYLE. Parenthetical performance or arrangement directions such as (soft guitar), (whispered), or (building intensity) are allowed. Do not invent unsupported tags or put descriptive text inside a tag.

Preserve every protected exact lyric segment character-for-character. Never translate, paraphrase, normalize spelling or merge it into STYLE.

For an instrumental, leave [LYRICS] empty. Express the entire musical arc in [STYLE]; do not write [Instrumental], because Maestro sends is_instrumental: true separately.

Length discipline:
Expand Down
36 changes: 36 additions & 0 deletions app/services/series_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import uuid
from typing import Any

from .language_intent import normalize_language_intent


SERIES_LIBRARY_FILENAME = ".series-library-v1.json"
MAX_SERIES_PROJECTS = 100
Expand All @@ -37,6 +39,12 @@
"negativePrompt", "audioDirection",
})
SHOT_SERVER_FIELDS = frozenset({"attempts", "approvedAttemptId", "referenceManifest"})
SERIES_CANON_INPUT_FIELDS = (
"title", "premise", "logline", "format", "language", "spokenLanguage",
"protagonistConsistency", "protagonistCharacterId", "genre", "tone", "audience",
"visualStyle", "characterVisualStyle", "cameraLanguage", "sourceMode",
"masterUniversePrompt", "characters", "relationships", "locations", "props",
)


class SeriesConflictError(ValueError):
Expand Down Expand Up @@ -115,6 +123,24 @@ def validate_series_asset_uri(value: Any) -> str:
return uri


def series_canon_inputs_changed(current: dict, updated: dict) -> bool:
"""Compare durable production inputs without coupling canon to chat language."""
current_canon = copy.deepcopy(current.get("canon") or {})
updated_canon = copy.deepcopy(updated.get("canon") or {})
for value in (current_canon, updated_canon):
value.pop("approval", None)
value.pop("approvedAt", None)
if current_canon != updated_canon:
return True
if any(current.get(key) != updated.get(key) for key in SERIES_CANON_INPUT_FIELDS):
return True
current_intent = normalize_language_intent(current.get("languageIntent"))
updated_intent = normalize_language_intent(updated.get("languageIntent"))
current_intent.pop("conversationLanguage", None)
updated_intent.pop("conversationLanguage", None)
return current_intent != updated_intent


def empty_series_library(workspace_id: str = "default") -> dict[str, Any]:
return {
"schema": "series-library",
Expand All @@ -139,6 +165,9 @@ def create_series_project(
"title": title.strip() or "Untitled series", "logline": "", "premise": "",
"format": "episodic", "defaultEpisodeDurationSeconds": 75,
"language": "Español", "spokenLanguage": "Español de España",
"languageIntent": normalize_language_intent(
None, content_language="Español", spoken_language="Español de España"
),
"protagonistConsistency": False, "protagonistCharacterId": "",
"genre": "", "tone": "Cinematic", "audience": "General",
"visualStyle": "", "characterVisualStyle": "", "cameraLanguage": "",
Expand Down Expand Up @@ -830,6 +859,13 @@ def normalize_series_project(value: Any, key: str, workspace_id: str) -> dict:
"spokenLanguage": _text(
project.get("spokenLanguage"), _text(project.get("language"), "Español de España")
),
"languageIntent": normalize_language_intent(
project.get("languageIntent"),
content_language=_text(project.get("language"), "Español"),
spoken_language=_text(
project.get("spokenLanguage"), _text(project.get("language"), "Español de España")
),
),
"protagonistConsistency": project.get("protagonistConsistency") is True,
"protagonistCharacterId": (
_text(project.get("protagonistCharacterId"))
Expand Down
Loading