Lunara
Subtitle engine for AI agents: one upload gives a written track and a translation, timings exact.
Should I use this
Quality & Safety
Findings (1)
- LOWin lunara_rewrite
Based on automated analysis of tool definitions and protocol compliance.
Context Cost
This is the approximate number of tokens consumed each time the server's tools are loaded into a model's context. Higher counts reduce the attention available for other tasks.
Install
One-Click Install
Add this to your `claude_desktop_config.json` file:
{
"mcpServers": {
"lunara": {
"url": "https://lunaraai.cc/api/lunara/mcp"
}
}
}Remote endpoints
https://lunaraai.cc/api/lunara/mcpstreamable-httphttps://lunaraai.cc/mcpstreamable-httpWhat it can do
Tool inventory
Tools (18)
🟢lunara_list_tools
Return every subtitle tool Lunara exposes, with the minutes each one costs. Call this first when you are unsure which tool fits.
Input Schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}🟢lunara_get_job(jobId)
Return the state of a subtitle job: status, progress, whether the SRT is ready, and how many cues it has. Poll this after lunara_upload_video.
Input Schema
{
"type": "object",
"properties": {
"jobId": {
"type": "string",
"description": "The job id returned by lunara_upload_video."
}
},
"required": [
"jobId"
],
"additionalProperties": false
}🟢lunara_get_srt(jobId, track)
Return the subtitle file for a finished job. One line per cue. Use track="secondary" for the translated track (English by default).
Input Schema
{
"type": "object",
"properties": {
"jobId": {
"type": "string",
"description": "The job id."
},
"track": {
"type": "string",
"enum": [
"primary",
"secondary"
],
"description": "primary = the written-language track; secondary = the translation."
}
},
"required": [
"jobId"
],
"additionalProperties": false
}🟢lunara_list_cues(jobId, offset, limit)
Return subtitle cues for a job as timestamps plus text, so you can inspect or reference individual lines before proposing edits.
Input Schema
{
"type": "object",
"properties": {
"jobId": {
"type": "string",
"description": "The job id."
},
"offset": {
"type": "integer",
"minimum": 0,
"description": "Skip this many cues. Default 0."
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 500,
"description": "How many cues to return. Default 100."
}
},
"required": [
"jobId"
],
"additionalProperties": false
}🟡lunara_start_subtitle_job(audioUrl, primaryLang, secondaryLang, spokenLang)
Run the full eight-layer subtitle pipeline on an audio or video URL you can already host. Produces a written-language track and a translated track. Prefer lunara_upload_video when the file is local.
Input Schema
{
"type": "object",
"properties": {
"audioUrl": {
"type": "string",
"description": "A URL Lunara can download (public or pre-signed)."
},
"primaryLang": {
"type": "string",
"enum": [
"zh-Hant",
"yue-Hant",
"zh-CN",
"en",
"ja",
"ko"
],
"description": "Language of the written track. Default yue-Hant."
},
"secondaryLang": {
"type": "string",
"enum": [
"zh-Hant",
"yue-Hant",
"zh-CN",
"en",
"ja",
"ko"
],
"description": "Language of the translated track. Default en."
},
"spokenLang": {
"type": "string",
"description": "What the speaker is speaking, e.g. yue-Hant-HK. Defaults to primaryLang."
}
},
"required": [
"audioUrl"
],
"additionalProperties": false
}🟡lunara_subtitle_from_url(audioUrl, filename, primaryLang, secondaryLang, spokenLang)
Start a persisted subtitle job from a URL you already host — no upload needed. Returns a job id; poll lunara_get_job every 15–30 s until status is "completed", then call lunara_get_srt. Prefer this over lunara_upload_video whenever the media is reachable by URL.
Input Schema
{
"type": "object",
"properties": {
"audioUrl": {
"type": "string",
"description": "Direct HTTP(S) URL to the audio or video file. Must be downloadable by our server."
},
"filename": {
"type": "string",
"description": "Display name for the result. Defaults to the URL basename."
},
"primaryLang": {
"type": "string",
"enum": [
"zh-Hant",
"yue-Hant",
"zh-CN",
"en",
"ja",
"ko"
],
"description": "Language of the written track. Default yue-Hant."
},
"secondaryLang": {
"type": "string",
"enum": [
"zh-Hant",
"yue-Hant",
"zh-CN",
"en",
"ja",
"ko"
],
"description": "Language of the translated track. Default en."
},
"spokenLang": {
"type": "string",
"description": "What the speaker is speaking, e.g. yue-Hant-HK. Defaults to auto-detect."
}
},
"required": [
"audioUrl"
],
"additionalProperties": false
}🟡lunara_upload_video(projectId, clientOperationId, filename, mime, sizeBytes, ...)
Reserve an upload slot and return the object key plus a signed URL per part. After uploading the bytes call lunara_finish_upload. Use this for a file you hold locally.
Input Schema
{
"type": "object",
"properties": {
"projectId": {
"type": "string",
"description": "Project to upload into. Omit to let Lunara reuse the most recent project for this organisation."
},
"clientOperationId": {
"type": "string",
"description": "Idempotency key. Reuse the same value to retry without creating a second upload."
},
"filename": {
"type": "string",
"description": "Original filename, used for the download name."
},
"mime": {
"type": "string",
"description": "MIME type, e.g. audio/mpeg, video/mp4."
},
"sizeBytes": {
"type": "integer",
"minimum": 1,
"description": "Exact file size in bytes."
},
"primaryLang": {
"type": "string",
"enum": [
"zh-Hant",
"yue-Hant",
"zh-CN",
"en",
"ja",
"ko"
],
"description": "Written-track language. Default yue-Hant."
},
"secondaryLang": {
"type": "string",
"enum": [
"zh-Hant",
"yue-Hant",
"zh-CN",
"en",
"ja",
"ko"
],
"description": "Translated-track language. Default en."
}
},
"required": [
"filename",
"mime",
"sizeBytes"
],
"additionalProperties": false
}🟡lunara_finish_upload(videoId, uploadId, key, durationMs, parts)
Tell Lunara the bytes are uploaded so the pipeline can start. Returns the job id to poll with lunara_get_job.
Input Schema
{
"type": "object",
"properties": {
"videoId": {
"type": "string",
"description": "The video id from lunara_upload_video."
},
"uploadId": {
"type": "string",
"description": "The multipart upload id."
},
"key": {
"type": "string",
"description": "The object key."
},
"durationMs": {
"type": "integer",
"minimum": 1,
"description": "Media duration in milliseconds."
},
"parts": {
"type": "array",
"description": "Uploaded parts, each { PartNumber, ETag }.",
"items": {
"type": "object",
"properties": {
"PartNumber": {
"type": "integer",
"minimum": 1
},
"ETag": {
"type": "string"
}
},
"required": [
"PartNumber",
"ETag"
],
"additionalProperties": false
}
}
},
"required": [
"videoId",
"uploadId",
"key",
"durationMs",
"parts"
],
"additionalProperties": false
}🟢lunara_get_glossary(limit)
Return the proper nouns Lunara has learned for this org, so names are spelled consistently across tracks.
Input Schema
{
"type": "object",
"properties": {
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 500,
"description": "Default 100."
}
},
"additionalProperties": false
}⚪lunara_transcribe(audioUrl, spokenLang)
Run speech recognition only — no rewrite, no translation. Returns timed segments. Use this when you want the transcript itself, or when you plan to pick a later stage yourself.
Input Schema
{
"type": "object",
"properties": {
"audioUrl": {
"type": "string",
"description": "Direct HTTP(S) URL to the audio or video file."
},
"spokenLang": {
"type": "string",
"description": "What the speaker is speaking, e.g. yue-Hant-HK. Defaults to auto-detect."
}
},
"required": [
"audioUrl"
],
"additionalProperties": false
}⚪lunara_translate(jobId, targetLang, track)
Translate the text of an existing job’s cues into another language. Every cue boundary and timestamp is preserved; only the text changes. Use this instead of translating yourself so the timing contract holds.
Input Schema
{
"type": "object",
"properties": {
"jobId": {
"type": "string",
"description": "The job whose cues to translate."
},
"targetLang": {
"type": "string",
"enum": [
"zh-Hant",
"en",
"ja",
"ko"
],
"description": "Target language."
},
"track": {
"type": "string",
"enum": [
"primary",
"secondary"
],
"description": "Which track to read. Default primary."
}
},
"required": [
"jobId",
"targetLang"
],
"additionalProperties": false
}⚪lunara_rewrite(jobId, track)
Turn spoken Cantonese cue text into written Chinese. Every cue boundary and timestamp is preserved. Deliberate mispronunciations are never corrected, and proper nouns are protected.
Input Schema
{
"type": "object",
"properties": {
"jobId": {
"type": "string",
"description": "The job whose cues to rewrite."
},
"track": {
"type": "string",
"enum": [
"primary",
"secondary"
],
"description": "Which track to read. Default primary."
}
},
"required": [
"jobId"
],
"additionalProperties": false
}⚪lunara_segment(jobId, maxCueMs, track)
Approximate split, not audio-grounded segmentation: cue text is divided at clause punctuation and each piece receives a share of the original span proportional to its character count. The words are not changed. Timings are estimated, so treat them as a reading aid rather than a re-alignment — use lunara_start_subtitle_job if you need timing derived from the audio.
Input Schema
{
"type": "object",
"properties": {
"jobId": {
"type": "string",
"description": "The job whose cues to re-cut."
},
"maxCueMs": {
"type": "integer",
"minimum": 500,
"maximum": 15000,
"description": "Duration ceiling per cue in ms. Default 7000."
},
"track": {
"type": "string",
"enum": [
"primary",
"secondary"
],
"description": "Which track to read. Default primary."
}
},
"required": [
"jobId"
],
"additionalProperties": false
}🟢lunara_glossary_extract(jobId, limit)
Find candidate proper nouns in a job’s cues, ranked by frequency, with the first cue they appear in. Suggestions for a glossary, not corrections — nothing is rewritten.
Input Schema
{
"type": "object",
"properties": {
"jobId": {
"type": "string",
"description": "The job whose cues to scan."
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 200,
"description": "Maximum terms to return. Default 50."
}
},
"required": [
"jobId"
],
"additionalProperties": false
}🟢lunara_qa_contract(jobId, track)
Run the contract gate over both tracks: ASR text and timings byte-exact, cue length bounds, no empty cues. Returns pass/fail per rule. Free, because refusing a broken track should never cost the caller money.
Input Schema
{
"type": "object",
"properties": {
"jobId": {
"type": "string",
"description": "The job to validate."
},
"track": {
"type": "string",
"enum": [
"primary",
"secondary",
"both"
],
"description": "Which track to check. Default both."
}
},
"required": [
"jobId"
],
"additionalProperties": false
}🟢lunara_probe_media(mediaUrl)
Return duration, dimensions, codecs and container for a URL, plus whether the audio is mono 16 kHz already. Call this before choosing an ASR engine.
Input Schema
{
"type": "object",
"properties": {
"mediaUrl": {
"type": "string",
"description": "URL to probe."
}
},
"required": [
"mediaUrl"
],
"additionalProperties": false
}🟢lunara_get_coverage(jobId)
Report how much of the source timeline the subtitles cover, and list suspicious gaps longer than a second. Use before shipping a long video.
Input Schema
{
"type": "object",
"properties": {
"jobId": {
"type": "string",
"description": "The job to inspect."
}
},
"required": [
"jobId"
],
"additionalProperties": false
}🟢lunara_readability(jobId, track)
Per-cue reading speed, displayed length and characters-per-second, with the cues that exceed comfortable limits. Mirrors the metrics shown in the editor.
Input Schema
{
"type": "object",
"properties": {
"jobId": {
"type": "string",
"description": "The job to measure."
},
"track": {
"type": "string",
"enum": [
"primary",
"secondary"
],
"description": "Default primary."
}
},
"required": [
"jobId"
],
"additionalProperties": false
}Community
Evidence