From a9eae2c34fa509efb6c06f288524c9aba17dad4c Mon Sep 17 00:00:00 2001 From: newjordan Date: Mon, 20 Jul 2026 11:13:32 -0500 Subject: [PATCH 1/3] feat: Shooting Schedule & Day Out of Days export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ScriptBreak's arc is breakdown → schedule → shoot, and the app already points at scheduling (the shot-list CSV is "for StripBoard, or your 1st AD"; the scene CSV is "the skeleton of a schedule") but never delivered it. This adds the missing step, as an additive export in the single index.html — no build step, no dependencies, no network. - Suggested stripboard: scenes grouped into synthetic shoot days by master location -> lighting regime (DAY / dusk-dawn "MAGIC" / NIGHT) -> INT vs EXT -> scene number, then split under a page budget (default 5 pg/day, +/-1 pg tolerance; one location per day). Print/PDF view + CSV. - Day Out of Days: the standard cast x shoot-day grid with SW / W / WF / SWF / H status codes and per-cast Work / Hold / Total(span) counts. Print/PDF + CSV. - New "Shooting Schedule & Day Out of Days" card in Export (with a pages/day control), both CSVs added to the "Everything" zip bundle, and a Help entry. Honest by design: cast presence is inferred from dialogue cues only, so silent/background cast are not detected and the DOOD under-reports who is needed; page counts may be estimated; and it does not model cast/location availability, company moves, or turnaround. Every output carries a prominent "draft — verify with your 1st AD" caveat. Conventions follow standard AD practice (Movie Magic-style grouping; standard DOOD SWF codes). Reuses exportScenes()/scopeLabel() (honors the scope bar), the openBreakdown- Sheets() print pattern, csvCell()/download(), and makeZip(). The scheduling helpers are duplicated verbatim in the MCP server (next commit) to preserve app<->MCP output parity. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01A2NtcuBy6SQmKaBQtgvkog --- README.md | 6 ++ src/index.html | 184 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 189 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f6c0695..6c430c3 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,12 @@ CSVs, or everything at once as a zip: Seedream, Midjourney). Paste one into ChatGPT, Claude, Gemini, a local model — no integration needed. Scope any export to scene ranges, page ranges, or your current filters, or download everything as one zip bundle. +- **Shooting schedule & Day Out of Days** — a draft stripboard that groups + scenes into shoot days by location, day/night, and int/ext under a + pages-per-day budget, plus the standard cast **Day Out of Days** grid + (Start / Work / Hold / Finish per shoot day). Print/PDF or CSV. It's a + starting point for your 1st AD — cast presence is read from dialogue, so + silent/background cast aren't detected; verify before scheduling. - **Zero setup** — no server, no database, no account. Install the app, load a script, and go. ## Why no API keys diff --git a/src/index.html b/src/index.html index 681d69a..c6f8fff 100644 --- a/src/index.html +++ b/src/index.html @@ -1953,6 +1953,7 @@
  • Storyboard Frame Pack — stills dialects: GPT Image 2, Nano Banana Pro, Krea 2, Seedream, Midjourney, or any image generator.
  • Coverage Consult — turns an LLM into a veteran DP/1st AD reviewing your shot list.
  • Script Companion — your whole breakdown as one context file to interrogate anywhere.
  • +
  • Shooting Schedule & Day Out of Days (print/PDF + CSV) — a draft stripboard grouping scenes into shoot days by location, day/night and int/ext under a page budget, plus the standard cast DOOD grid (Start / Work / Hold / Finish). Cast presence is read from dialogue only, so silent/background cast aren’t detected — a starting point for your 1st AD, not a locked schedule.
  • Breakdown sheets (print/PDF), shot list and scene CSVs, and the .scriptbreak project file.
  • The Scope bar limits any export to scene ranges (1-20, 34, 50A), page ranges, scenes with shots, or your current filters. Everything downloads the lot as one zip.

    `], @@ -2559,6 +2560,7 @@ /* ---------- export scope ---------- */ let EXP = { mode:'all', range:'' }; +let SCHED = { pagesPerDay:5 }; // target page budget per synthetic shoot day (schedule/DOOD) function scenePageStart(i){ if (S.scenes[i] && S.scenes[i].page) return S.scenes[i].page; let e = 0; for (let j=0;j lighting regime +// (DAY / MAGIC=dusk-dawn / NIGHT) -> INT vs EXT -> scene#, then break into +// ~pagesPerDay days (one location per day, ±1 page soft overflow). +function buildScheduleDays(scenes, opts){ + opts = opts||{}; + const target = Math.max(8, Math.round((opts.pagesPerDay||5)*8)); + const tol = (opts.tolerance!=null ? opts.tolerance : 8); + const castOrder = [], castId = {}; + scenes.forEach(sc=>(sc.characters||[]).forEach(c=>{ if (!(c in castId)){ castId[c]=castOrder.length+1; castOrder.push(c); } })); + const locFirst = new Map(); + scenes.forEach((sc,i)=>{ const k = schedLocKey(sc); if (!locFirst.has(k)) locFirst.set(k,i); }); + const groups = new Map(); + scenes.forEach(sc=>{ const loc = schedLocKey(sc), reg = schedRegime(sc), key = loc+' '+reg; + if (!groups.has(key)) groups.set(key, { loc, regime:reg, scenes:[] }); + groups.get(key).scenes.push(sc); }); + const groupList = [...groups.values()].sort((a,b)=>{ + const la = locFirst.get(a.loc), lb = locFirst.get(b.loc); + if (la!==lb) return la-lb; + return SCHED_REGIME_RANK[a.regime]-SCHED_REGIME_RANK[b.regime]; }); + groupList.forEach(g=>g.scenes.sort((a,b)=>{ + const ra = schedIeRank(a,g.regime), rb = schedIeRank(b,g.regime); + if (ra!==rb) return ra-rb; + const ka = schedNatKey(a.num), kb = schedNatKey(b.num); + if (ka[0]!==kb[0]) return ka[0]-kb[0]; + return ka[1]kb[1]?1:0; })); + const days = []; + for (const g of groupList){ + let cur = null; + for (const sc of g.scenes){ const e = sc.eighths||0; + if (!cur || (cur.eighths>0 && cur.eighths+e > target+tol)){ cur = { loc:g.loc, regime:g.regime, scenes:[], eighths:0 }; days.push(cur); } + cur.scenes.push(sc); cur.eighths += e; } + } + const rows = days.map((d,idx)=>{ + const cast = new Set(); + d.scenes.forEach(sc=>(sc.characters||[]).forEach(c=>cast.add(c))); + const ieset = [...new Set(d.scenes.map(schedIeLabel))]; + return { n:idx+1, location:d.loc, regime:d.regime, regimeLabel:SCHED_REGIME_LABEL[d.regime], + ie:ieset.join(' / '), sceneNums:d.scenes.map(sc=>sc.num), eighths:d.eighths, + castNames:castOrder.filter(c=>cast.has(c)), castIds:castOrder.filter(c=>cast.has(c)).map(c=>castId[c]) }; + }); + return { days:rows, castOrder, castId, targetEighths:target, tolerance:tol }; +} +// Day Out of Days from a plan. Codes: SW start · W work · WF finish · SWF one-day · H hold (paid). +function buildDood(plan){ + const dayCount = plan.days.length; + const worksOn = {}; + plan.castOrder.forEach(c=>worksOn[c]=new Set()); + plan.days.forEach((d,di)=>d.castNames.forEach(c=>worksOn[c].add(di))); + const cast = plan.castOrder.map(name=>{ + const set = worksOn[name], wd = [...set].sort((a,b)=>a-b); + const codes = new Array(dayCount).fill(''); + let work = 0, hold = 0; + if (wd.length){ const first = wd[0], last = wd[wd.length-1]; + for (let d=first; d<=last; d++){ + if (set.has(d)){ codes[d] = first===last?'SWF':d===first?'SW':d===last?'WF':'W'; work++; } + else { codes[d] = 'H'; hold++; } } } + return { id:plan.castId[name], name, codes, work, hold, total:work+hold }; + }); + return { dayCount, cast }; +} +function buildScheduleCSV(scenes, opts){ + const plan = buildScheduleDays(scenes, opts); + const rows = [['Day','Location','I/E','Day/Night','Scenes','Pages (1/8)','Cast IDs','Cast']]; + plan.days.forEach(d=>rows.push([d.n, d.location, d.ie, d.regimeLabel, d.sceneNums.join(' '), eighthsFmt(d.eighths), d.castIds.join(' '), d.castNames.join('; ')])); + return rows.map(r=>r.map(csvCell).join(',')).join('\n'); +} +function buildDoodCSV(scenes, opts){ + const plan = buildScheduleDays(scenes, opts), dood = buildDood(plan); + const header = ['ID','Character', ...plan.days.map(d=>'Day '+d.n), 'Work','Hold','Total']; + const rows = [header]; + dood.cast.forEach(c=>rows.push([c.id, c.name, ...c.codes, c.work, c.hold, c.total])); + return rows.map(r=>r.map(csvCell).join(',')).join('\n'); +} + +const SCHED_CAVEAT = 'Draft stripboard generated from ScriptBreak’s auto-parsed breakdown. Cast presence is inferred from dialogue only — silent / background cast are not detected, and page counts may be estimated. This is a starting point, not a locked schedule: it does not account for cast or location availability, company moves, day↔night turnaround, or child / stunt constraints. Verify with your 1st AD before scheduling.'; + +function schedPrintHead(title){ + return `${esc(S.title)} — ${esc(title)}`; +} +function schedCastKey(plan){ + return plan.castOrder.length + ? `
    Cast key  ` + plan.castOrder.map(c=>`${plan.castId[c]} ${esc(c)}`).join('  ·  ') + `
    ` + : ''; +} +function openScheduleSheet(){ + const w = window.open('', '_blank'); + if (!w){ toast('Popup blocked — allow popups to print the schedule'); return; } + const plan = buildScheduleDays(exportScenes(), SCHED); + const totalE = plan.days.reduce((n,d)=>n+d.eighths,0); + const locs = new Set(plan.days.map(d=>d.location)); + const moves = plan.days.reduce((n,d,i)=> n + (i>0 && plan.days[i-1].location!==d.location ? 1 : 0), 0); + const daysHtml = plan.days.map(d=>` +
    + DAY ${d.n} + ${esc(d.location)} · ${esc(d.ie)} · ${esc(d.regimeLabel)} · ${eighthsFmt(d.eighths)} pg
    +
    +
    Scenes${d.sceneNums.map(esc).join(', ')}
    +
    Cast${d.castIds.length? d.castIds.map(i=>`${i}`).join(', ') : '—'}${d.castNames.length? ' ('+d.castNames.map(esc).join(', ')+')':''}
    +
    `).join(''); + w.document.write(schedPrintHead('Shooting Schedule') + + `

    ${esc(S.title)} — Suggested Shooting Schedule

    +
    ${esc(scopeLabel())} · target ${SCHED.pagesPerDay} pg/day · generated ${new Date().toLocaleDateString()}
    +
    ${esc(SCHED_CAVEAT)}
    +
    +
    ${plan.days.length}
    Shoot days
    +
    ${(totalE/8).toFixed(1)}
    Pages
    +
    ${locs.size}
    Locations
    +
    ${moves}
    Location changes
    +
    ${daysHtml}${schedCastKey(plan)}