recappi 0.1.50 → 0.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -593,7 +593,7 @@ var init_LiveCaptionsScreen = __esm({
593
593
 
594
594
  // src/tui/RecordingHeroScreen.tsx
595
595
  import { useEffect as useEffect2, useRef, useState as useState3 } from "react";
596
- import { Box as Box2, Text as Text2 } from "ink";
596
+ import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
597
597
  import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
598
598
  function waveRowsFor(terminalRows) {
599
599
  return terminalRows >= 30 ? 5 : 3;
@@ -667,7 +667,13 @@ function RecordingHeroScreen({
667
667
  const [tick, setTick] = useState3(() => now());
668
668
  const [waveSys, setWaveSys] = useState3([]);
669
669
  const [waveMic, setWaveMic] = useState3([]);
670
+ const [captionMode, setCaptionMode] = useState3("both");
670
671
  const lastAppendRef = useRef(0);
672
+ useInput2((input) => {
673
+ if (input === "c") {
674
+ setCaptionMode((m) => m === "both" ? "source" : m === "source" ? "translation" : "both");
675
+ }
676
+ });
671
677
  useEffect2(() => {
672
678
  if (telemetry.level == null) return;
673
679
  const t = now();
@@ -750,55 +756,77 @@ function RecordingHeroScreen({
750
756
  /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: context }) }),
751
757
  captions ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
752
758
  /* @__PURE__ */ jsx3(Text2, { bold: true, dimColor: true, children: "LIVE CAPTIONS" }),
753
- /* @__PURE__ */ jsx3(HeroCaptions, { state: captions, maxRows: captionRows, width: innerWidth })
759
+ /* @__PURE__ */ jsx3(HeroCaptions, { state: captions, maxRows: captionRows, width: innerWidth, mode: captionMode })
754
760
  ] }) : null
755
761
  ] }),
756
762
  /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
757
763
  "q stop & save",
758
- canPause ? ` \xB7 p ${paused ? "resume" : "pause"}` : ""
764
+ canPause ? ` \xB7 p ${paused ? "resume" : "pause"}` : "",
765
+ captions ? " \xB7 c captions" : ""
759
766
  ] }) })
760
767
  ] });
761
768
  }
762
769
  function wrappedRows(text, width) {
763
770
  return Math.max(1, Math.ceil(displayWidth(text) / Math.max(1, width)));
764
771
  }
772
+ function captionColumn(items, maxRows, width, dim) {
773
+ const budget = Math.max(1, maxRows);
774
+ const chosen = [];
775
+ let used = 0;
776
+ for (let i = items.length - 1; i >= 0; i--) {
777
+ const h = wrappedRows(items[i].text, width);
778
+ if (used + h > budget && chosen.length > 0) break;
779
+ chosen.unshift(items[i]);
780
+ used += h;
781
+ }
782
+ return chosen.map((it) => /* @__PURE__ */ jsx3(Text2, { dimColor: dim, wrap: "wrap", children: it.text }, it.key));
783
+ }
765
784
  function HeroCaptions({
766
785
  state,
767
786
  maxRows,
768
- width
787
+ width,
788
+ mode
769
789
  }) {
770
790
  const hasPartial = Boolean(state.partial && state.partial.length > 0);
771
791
  const captionError = state.status === "error" ? `Captions unavailable: ${state.error ?? "Live captions unavailable."}` : null;
772
792
  if (state.lines.length === 0 && !hasPartial) {
773
793
  return /* @__PURE__ */ jsx3(Text2, { color: captionError ? "yellow" : void 0, dimColor: !captionError, children: captionError ?? (state.status === "live" ? "Listening for speech\u2026" : liveCaptionStatusLabel(state.status)) });
774
794
  }
775
- const lines = [];
776
- for (const line of state.lines) {
777
- lines.push({
778
- key: `${line.id}-s`,
779
- text: `${line.speaker ? `${line.speaker}: ` : ""}${trimLead(line.text)}`,
780
- dim: false
781
- });
782
- if (line.translation) {
783
- lines.push({ key: `${line.id}-t`, text: ` \u21B3 ${trimLead(line.translation)}`, dim: true });
784
- }
785
- }
786
- if (hasPartial) lines.push({ key: "partial", text: trimLead(state.partial), dim: true });
787
- if (state.translationPartial) {
788
- lines.push({ key: "tpartial", text: ` \u21B3 ${trimLead(state.translationPartial)}`, dim: true });
795
+ const sourceItems = state.lines.map((l) => ({
796
+ key: `${l.id}-s`,
797
+ text: `${l.speaker ? `${l.speaker}: ` : ""}${trimLead(l.text)}`
798
+ }));
799
+ if (hasPartial) sourceItems.push({ key: "sp", text: trimLead(state.partial) });
800
+ const translationItems = state.lines.filter((l) => l.translation).map((l) => ({ key: `${l.id}-t`, text: trimLead(l.translation) }));
801
+ if (state.translationPartial) translationItems.push({ key: "tp", text: trimLead(state.translationPartial) });
802
+ const errLine = captionError ? /* @__PURE__ */ jsx3(Text2, { color: "yellow", wrap: "wrap", children: captionError }) : null;
803
+ const hasTranslation = translationItems.length > 0;
804
+ if (mode === "source" || !hasTranslation) {
805
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
806
+ captionColumn(sourceItems, maxRows, width, false),
807
+ errLine
808
+ ] });
789
809
  }
790
- const budget = Math.max(1, maxRows);
791
- const chosen = [];
792
- let used = 0;
793
- for (let i = lines.length - 1; i >= 0; i--) {
794
- const h = wrappedRows(lines[i].text, width);
795
- if (used + h > budget && chosen.length > 0) break;
796
- chosen.unshift(lines[i]);
797
- used += h;
810
+ if (mode === "translation") {
811
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
812
+ captionColumn(translationItems, maxRows, width, false),
813
+ errLine
814
+ ] });
798
815
  }
816
+ const gap = 2;
817
+ const colW = Math.max(12, Math.floor((width - gap) / 2));
799
818
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
800
- chosen.map((l) => /* @__PURE__ */ jsx3(Text2, { dimColor: l.dim, wrap: "wrap", children: l.text }, l.key)),
801
- captionError ? /* @__PURE__ */ jsx3(Text2, { color: "yellow", wrap: "wrap", children: captionError }) : null
819
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", children: [
820
+ /* @__PURE__ */ jsxs2(Box2, { width: colW, flexDirection: "column", marginRight: gap, children: [
821
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "ORIGINAL" }),
822
+ captionColumn(sourceItems, Math.max(1, maxRows - 1), colW, false)
823
+ ] }),
824
+ /* @__PURE__ */ jsxs2(Box2, { width: colW, flexDirection: "column", children: [
825
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "TRANSLATION" }),
826
+ captionColumn(translationItems, Math.max(1, maxRows - 1), colW, false)
827
+ ] })
828
+ ] }),
829
+ errLine
802
830
  ] });
803
831
  }
804
832
  function stoppedHandoffCopy(artifact, canTranscribe) {
@@ -1392,7 +1420,7 @@ var init_JobDetailView = __esm({
1392
1420
 
1393
1421
  // src/tui/RecordingDetailView.tsx
1394
1422
  import React6, { useMemo as useMemo2, useState as useState4 } from "react";
1395
- import { Box as Box12, Text as Text12, useInput as useInput3 } from "ink";
1423
+ import { Box as Box12, Text as Text12, useInput as useInput4 } from "ink";
1396
1424
  import { Fragment as Fragment5, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
1397
1425
  function RecordingDetailView({
1398
1426
  item,
@@ -1436,7 +1464,7 @@ function RecordingDetailView({
1436
1464
  setScroll(found < 0 ? Math.max(0, segments.length - 1) : found);
1437
1465
  setTab("transcript");
1438
1466
  };
1439
- useInput3((input, key) => {
1467
+ useInput4((input, key) => {
1440
1468
  if (!item.activeTranscriptId || !ready) return;
1441
1469
  if (key.tab) {
1442
1470
  setTab((t) => TAB_ORDER[(TAB_ORDER.indexOf(t) + (key.shift ? TAB_ORDER.length - 1 : 1)) % TAB_ORDER.length]);
@@ -1586,7 +1614,7 @@ var init_RecordingDetailView = __esm({
1586
1614
 
1587
1615
  // src/tui/TranscriptView.tsx
1588
1616
  import { useMemo as useMemo3, useState as useState5 } from "react";
1589
- import { Box as Box13, Text as Text13, useInput as useInput4 } from "ink";
1617
+ import { Box as Box13, Text as Text13, useInput as useInput5 } from "ink";
1590
1618
  import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
1591
1619
  function TranscriptView({ loading, data, error: error51 }) {
1592
1620
  const size = useTerminalSize();
@@ -1603,7 +1631,7 @@ function TranscriptView({ loading, data, error: error51 }) {
1603
1631
  const budget = Math.max(3, size.rows - 3);
1604
1632
  const win = windowByHeights(heights, scroll, budget);
1605
1633
  const page = Math.max(1, budget - 1);
1606
- useInput4((input, key) => {
1634
+ useInput5((input, key) => {
1607
1635
  if (key.downArrow || input === "j") setScroll((s) => Math.min(win.maxScroll, s + 1));
1608
1636
  else if (key.upArrow || input === "k") setScroll((s) => Math.max(0, s - 1));
1609
1637
  else if (key.pageDown || input === " ") setScroll((s) => Math.min(win.maxScroll, s + page));
@@ -1730,7 +1758,7 @@ var init_PermissionPreflightView = __esm({
1730
1758
 
1731
1759
  // src/tui/RecordSetupView.tsx
1732
1760
  import { useEffect as useEffect3, useRef as useRef2, useState as useState6 } from "react";
1733
- import { Box as Box15, Text as Text15, useInput as useInput5 } from "ink";
1761
+ import { Box as Box15, Text as Text15, useInput as useInput6 } from "ink";
1734
1762
  import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
1735
1763
  function levelDb2(level) {
1736
1764
  if (level <= 0.03) return "silent";
@@ -1793,7 +1821,7 @@ function RecordSetupView({
1793
1821
  const di = microphones.findIndex((device) => device.isDefault);
1794
1822
  if (di > 0) setMicIdx(di);
1795
1823
  }, [microphones.length]);
1796
- useInput5((input, key) => {
1824
+ useInput6((input, key) => {
1797
1825
  if (key.upArrow || input === "k") setSrcIdx((i) => Math.max(0, i - 1));
1798
1826
  else if (key.downArrow || input === "j") setSrcIdx((i) => Math.min(sources.length - 1, i + 1));
1799
1827
  else if (input === " ") setIncludeMic((m) => !m);
@@ -1871,8 +1899,8 @@ var init_RecordSetupView = __esm({
1871
1899
  });
1872
1900
 
1873
1901
  // src/tui/AppShell.tsx
1874
- import { useCallback, useEffect as useEffect4, useState as useState7 } from "react";
1875
- import { Box as Box16, Text as Text16, useApp, useInput as useInput6 } from "ink";
1902
+ import { useCallback, useEffect as useEffect4, useRef as useRef3, useState as useState7 } from "react";
1903
+ import { Box as Box16, Text as Text16, useApp, useInput as useInput7 } from "ink";
1876
1904
  import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
1877
1905
  function recordErrorCopy(code, message) {
1878
1906
  switch (code) {
@@ -2071,6 +2099,7 @@ function AppShell({
2071
2099
  bySourceId: {},
2072
2100
  byMicrophoneId: {}
2073
2101
  });
2102
+ const autoTranscribeStartedSessionIds = useRef3(/* @__PURE__ */ new Set());
2074
2103
  const recordSetupModel = {
2075
2104
  sources: recordSetupInputs.sources.length > 0 ? recordSetupInputs.sources : DEFAULT_RECORDING_SOURCES,
2076
2105
  microphones: recordSetupInputs.microphones ?? [],
@@ -2370,6 +2399,17 @@ function AppShell({
2370
2399
  setNotice("Transcription failed. Press enter to retry.");
2371
2400
  }
2372
2401
  }, [liveRecord, refresh, transcribeRecordingArtifact]);
2402
+ useEffect4(() => {
2403
+ if (liveRecord?.kind !== "stopped") return;
2404
+ const artifact = liveRecord.artifact;
2405
+ if (!artifact?.audioPath || !transcribeRecordingArtifact) return;
2406
+ if (artifact.uploadStatus !== "local_only" || artifact.transcriptionStatus !== "not_started") {
2407
+ return;
2408
+ }
2409
+ if (autoTranscribeStartedSessionIds.current.has(artifact.sessionId)) return;
2410
+ autoTranscribeStartedSessionIds.current.add(artifact.sessionId);
2411
+ void transcribeStoppedRecording();
2412
+ }, [liveRecord, transcribeRecordingArtifact, transcribeStoppedRecording]);
2373
2413
  const loadMoreRecordings = useCallback(async () => {
2374
2414
  if (!fetchRecordings || !recordingsNextCursor || loadingMoreRecordings) return;
2375
2415
  setLoadingMoreRecordings(true);
@@ -2516,7 +2556,7 @@ function AppShell({
2516
2556
  setNotice(void 0);
2517
2557
  };
2518
2558
  const back = () => setStack((st) => st.length > 1 ? st.slice(0, -1) : st);
2519
- useInput6((input, key) => {
2559
+ useInput7((input, key) => {
2520
2560
  setNotice(void 0);
2521
2561
  if (screen.kind === "recordSetup") {
2522
2562
  if (input === "q" || key.leftArrow) back();
@@ -17763,6 +17803,9 @@ var recordCommandDataSchema = external_exports.object({
17763
17803
  sessionId: external_exports.string(),
17764
17804
  state: sidecarRecordingStateSchema,
17765
17805
  recordingId: external_exports.string().optional(),
17806
+ jobId: external_exports.string().optional(),
17807
+ transcriptId: external_exports.string().optional(),
17808
+ cloudHandoffError: cliErrorDescriptorSchema.optional(),
17766
17809
  localSessionRef: external_exports.string().optional(),
17767
17810
  sidecar: sidecarInfoSchema.optional(),
17768
17811
  artifacts: external_exports.array(sidecarLocalArtifactSchema)
@@ -20052,6 +20095,12 @@ Next:
20052
20095
  opts.stdout("Recording complete\n");
20053
20096
  if (typeof data.recordingId === "string") opts.stdout(` recordingId: ${data.recordingId}
20054
20097
  `);
20098
+ if (typeof data.jobId === "string") opts.stdout(` jobId: ${data.jobId}
20099
+ `);
20100
+ if (typeof data.transcriptId === "string") {
20101
+ opts.stdout(` transcriptId: ${data.transcriptId}
20102
+ `);
20103
+ }
20055
20104
  if (typeof data.sessionId === "string") opts.stdout(` sessionId: ${data.sessionId}
20056
20105
  `);
20057
20106
  if (typeof data.localSessionRef === "string") {
@@ -20068,7 +20117,24 @@ Next:
20068
20117
  `);
20069
20118
  }
20070
20119
  }
20071
- if (typeof data.recordingId === "string") {
20120
+ const cloudHandoffError = isRecord4(data.cloudHandoffError) ? data.cloudHandoffError : void 0;
20121
+ if (cloudHandoffError && typeof cloudHandoffError.message === "string") {
20122
+ opts.stderr(`Cloud handoff failed: ${cloudHandoffError.message}
20123
+ `);
20124
+ if (typeof cloudHandoffError.hint === "string") opts.stderr(`${cloudHandoffError.hint}
20125
+ `);
20126
+ }
20127
+ if (typeof data.transcriptId === "string") {
20128
+ opts.stdout(`
20129
+ Next:
20130
+ recappi transcript get ${data.transcriptId}
20131
+ `);
20132
+ } else if (typeof data.jobId === "string") {
20133
+ opts.stdout(`
20134
+ Next:
20135
+ recappi jobs wait ${data.jobId}
20136
+ `);
20137
+ } else if (typeof data.recordingId === "string") {
20072
20138
  opts.stdout(`
20073
20139
  Next:
20074
20140
  recappi recordings get ${data.recordingId}
@@ -20490,7 +20556,7 @@ import { createRequire as createRequire2 } from "module";
20490
20556
  import { homedir } from "os";
20491
20557
  import { dirname, join as join2 } from "path";
20492
20558
  import { fileURLToPath } from "url";
20493
- import { render, useInput as useInput2 } from "ink";
20559
+ import { render, useInput as useInput3 } from "ink";
20494
20560
  init_recordingCore();
20495
20561
 
20496
20562
  // src/sidecar.ts
@@ -21346,7 +21412,7 @@ function RecordLiveScreen({
21346
21412
  onStop,
21347
21413
  now
21348
21414
  }) {
21349
- useInput2((input, key) => {
21415
+ useInput3((input, key) => {
21350
21416
  if (input === "q" || key.escape || key.leftArrow) onStop();
21351
21417
  });
21352
21418
  return /* @__PURE__ */ jsx4(LiveCaptionsScreen, { source, now });
@@ -21405,7 +21471,7 @@ function RecordingHeroLive({
21405
21471
  if (mapped) setCaptions((prev) => liveCaptionReducer(prev, mapped));
21406
21472
  });
21407
21473
  }, [captionStreamEnabled, source]);
21408
- useInput2((input, key) => {
21474
+ useInput3((input, key) => {
21409
21475
  if (input === "q" || key.escape) onStop();
21410
21476
  });
21411
21477
  return /* @__PURE__ */ jsx4(
@@ -21419,7 +21485,50 @@ function RecordingHeroLive({
21419
21485
  }
21420
21486
 
21421
21487
  // src/cli.ts
21488
+ init_recordingCore();
21422
21489
  var DASHBOARD_RECORDINGS_PAGE_SIZE = 50;
21490
+ async function uploadRecordedSessionAfterStop(client, data, opts = {}) {
21491
+ const artifact = recordingArtifactFromRecordData(data);
21492
+ if (!artifact.audioPath) return data;
21493
+ try {
21494
+ const upload = await client.uploadPathBatch({
21495
+ inputPath: artifact.audioPath,
21496
+ transcribe: true,
21497
+ wait: false,
21498
+ ...opts.title ? { title: opts.title } : {},
21499
+ ...opts.language ? { language: opts.language } : {},
21500
+ onEvent: opts.onEvent
21501
+ });
21502
+ if (upload.failures.length > 0) {
21503
+ const failure = upload.failures[0];
21504
+ return recordCommandDataSchema.parse({
21505
+ ...data,
21506
+ cloudHandoffError: failure.error
21507
+ });
21508
+ }
21509
+ const success2 = upload.successes[0];
21510
+ if (!success2) {
21511
+ return recordCommandDataSchema.parse({
21512
+ ...data,
21513
+ cloudHandoffError: cliError(
21514
+ "input.unsupported_audio",
21515
+ "No supported local audio file was uploaded."
21516
+ ).descriptor
21517
+ });
21518
+ }
21519
+ return recordCommandDataSchema.parse({
21520
+ ...data,
21521
+ recordingId: success2.recordingId,
21522
+ ...success2.jobId ? { jobId: success2.jobId } : {},
21523
+ ...success2.transcriptId ? { transcriptId: success2.transcriptId } : {}
21524
+ });
21525
+ } catch (error51) {
21526
+ return recordCommandDataSchema.parse({
21527
+ ...data,
21528
+ cloudHandoffError: toCliError(error51).descriptor
21529
+ });
21530
+ }
21531
+ }
21423
21532
  async function runCli(deps = {}) {
21424
21533
  const argv = deps.argv ?? process.argv.slice(2);
21425
21534
  const stdout = deps.stdout ?? ((text) => process.stdout.write(text));
@@ -21632,7 +21741,7 @@ async function runCli(deps = {}) {
21632
21741
  });
21633
21742
  }
21634
21743
  const translationLanguage = parsed.translationLanguage ?? (mode === "human" && isTTY ? "zh" : void 0);
21635
- const data = await recordViaSidecar({
21744
+ const captured = await recordViaSidecar({
21636
21745
  account: {
21637
21746
  backendOrigin: auth.origin,
21638
21747
  userId: status.userId,
@@ -21654,6 +21763,11 @@ async function runCli(deps = {}) {
21654
21763
  requireLiveCaptions: parsed.live === true,
21655
21764
  runtime: deps.recordRuntime
21656
21765
  });
21766
+ const data = await uploadRecordedSessionAfterStop(client, captured, {
21767
+ title: parsed.title,
21768
+ language: parsed.transcriptionLanguage,
21769
+ onEvent: (event) => renderEvent(event, render3)
21770
+ });
21657
21771
  renderSuccess("record", data, render3);
21658
21772
  return 0;
21659
21773
  }