Skip to content

Commit

Permalink
Add MediaExcerpt-based Justifications
Browse files Browse the repository at this point in the history
Signed-off-by: Carl Gieringer <78054+carlgieringer@users.noreply.github.com>
  • Loading branch information
carlgieringer committed Jun 18, 2023
1 parent c6a9c66 commit 76d08d7
Show file tree
Hide file tree
Showing 32 changed files with 539 additions and 167 deletions.
3 changes: 3 additions & 0 deletions howdju-common/lib/apiModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export const ExternalJustificationSearchFilters = [
"writId",
// Justifications based on this PropositionCompound
"propositionCompoundId",
"mediaExcerptId",
"sourceExcerptParaphraseId",
// Justifications based on this proposition in a PropositionCompound
"propositionId",
Expand All @@ -144,3 +145,5 @@ export interface SortDescription {
}

export type PersorgOut = Persisted<Persorg>;

export type TagOut = Persisted<Tag>;
3 changes: 2 additions & 1 deletion howdju-common/lib/contextTrails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,9 @@ export function areValidTargetAndConnectingEntity(
prev.entity.basis.entity.atoms,
(a) => a.entity.id === id
);
case "SOURCE_EXCERPT":
case "WRIT_QUOTE":
// TODO(20): when we add Appearances, connect them to MediaExcerpts here.
case "MEDIA_EXCERPT":
return false;
}
}
Expand Down
1 change: 1 addition & 0 deletions howdju-common/lib/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const JustificationBasisSourceTypes = {
PROPOSITION: "PROPOSITION",
/** @deprecated TODO(215) */
SOURCE_EXCERPT_PARAPHRASE: "SOURCE_EXCERPT_PARAPHRASE",
MEDIA_EXCERPT: "MEDIA_EXCERPT",
} as const;
export type JustificationBasisSourceType =
typeof JustificationBasisSourceTypes[keyof typeof JustificationBasisSourceTypes];
Expand Down
20 changes: 20 additions & 0 deletions howdju-common/lib/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ export const hasQuote = (j: Justification) =>
export const isPropositionCompoundBased = (
j: Justification | CreateJustification | CreateJustificationInput
) => (j ? j.basis.type === "PROPOSITION_COMPOUND" : false);

export function isMediaExcerptBased(
j: Justification | CreateJustification | CreateJustificationInput
) {
return j.basis.type === "MEDIA_EXCERPT";
}

export const isWritQuoteBased = (
j: Justification | CreateJustification | CreateJustificationInput
) => (j ? j.basis.type === "WRIT_QUOTE" : false);
Expand Down Expand Up @@ -483,6 +490,11 @@ const muxCreateJustificationBasisErrors = (
_errors: errors._errors,
propositionCompound: errors.entity,
};
case "MEDIA_EXCERPT":
return {
_errors: errors._errors,
mediaExcerpt: errors.entity,
};
case "WRIT_QUOTE":
return {
_errors: errors._errors,
Expand Down Expand Up @@ -533,6 +545,14 @@ const demuxCreateJustificationInputBasis = (
type: "PROPOSITION_COMPOUND",
entity: basis.propositionCompound,
};
case "MEDIA_EXCERPT":
if (!basis.mediaExcerpt) {
throw newImpossibleError("Media excerpt must be defined.");
}
return {
type: "MEDIA_EXCERPT",
entity: basis.mediaExcerpt,
};
case "WRIT_QUOTE":
// TODO(201) WritQuote bases are temporarily supported until we support SourceExcerpt bases.
return {
Expand Down
42 changes: 40 additions & 2 deletions howdju-common/lib/viewModels.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { JustificationOut, PropositionOut, StatementOut } from "./apiModels";
import {
JustificationOut,
MediaExcerptOut,
PropositionCompoundOut,
PropositionOut,
StatementOut,
WritQuoteOut,
} from "./apiModels";

/** A JustificationOut that has been joined with its root target in the client */
export type JustificationView = Omit<
JustificationOut,
"rootTarget" | "rootTargetType" | "target"
"rootTarget" | "rootTargetType" | "target" | "basis"
> &
(
| {
Expand All @@ -28,4 +35,35 @@ export type JustificationView = Omit<
type: "JUSTIFICATION";
entity: JustificationView;
};
} & {
basis:
| {
type: "PROPOSITION_COMPOUND";
entity: PropositionCompoundOut;
}
| {
type: "MEDIA_EXCERPT";
entity: MediaExcerptView;
}
| {
type: "WRIT_QUOTE";
entity: WritQuoteOut;
};
};

export interface MediaExcerptView extends MediaExcerptOut {
citations: (MediaExcerptOut["citations"][number] & {
/** A key uniquely identifying a citation relative to others. */
key: string;
})[];
locators: MediaExcerptOut["locators"] & {
urlLocators: (MediaExcerptOut["locators"]["urlLocators"][number] & {
/** A key uniquely identifying a url locator relative to others. */
key: string;
})[];
};
speakers: (MediaExcerptOut["speakers"][number] & {
/** A key uniquely identifying a persorg relative to others. */
key: string;
})[];
}
9 changes: 8 additions & 1 deletion howdju-common/lib/zodSchemaTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ export type PersistedJustificationWithRootRef = Omit<
type: "PROPOSITION_COMPOUND";
entity: Persisted<PropositionCompound>;
}
| { type: "SOURCE_EXCERPT"; entity: Persisted<SourceExcerpt> }
| {
type: "MEDIA_EXCERPT";
entity: Persisted<MediaExcerpt>;
}
| { type: "WRIT_QUOTE"; entity: Persisted<WritQuote> };
};

Expand Down Expand Up @@ -121,6 +124,10 @@ export type BasedJustificationWithRootRef = Omit<
type: "PROPOSITION_COMPOUND";
entity: Persisted<PropositionCompound>;
}
| {
type: "MEDIA_EXCERPT";
entity: Persisted<MediaExcerpt>;
}
| { type: "SOURCE_EXCERPT"; entity: Persisted<SourceExcerpt> }
| { type: "WRIT_QUOTE"; entity: Persisted<WritQuote> };
};
Expand Down
68 changes: 58 additions & 10 deletions howdju-common/lib/zodSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,25 @@ export type CreateMediaExcerptCitationInput = z.output<
typeof CreateMediaExcerptCitationInput
>;

/** A representation of an excerpt of some fixed media conveying speech. */
/**
* A representation of an excerpt of some fixed media conveying speech. *
*
* Two MediaExcerpts are equivalent if they represent the same 'speech act'. A 'speech act' is a
* single event of a particular speaker saying a particular thing at a particular time. The
* MediaExcerpts are only equivalent if the media represents the same direct speech. For example,
* if two translators translate the same speech into different languages, MediaExcerpts of those
* translations are not necessarily equivalent because the translators may have introduced their own
* interpretation on top of the original speech.
*
* The same speaker saying the same thing at a different time is a different speech act.
*
* Two MediaExcerpts are equivalent if they represent the same speech in the same part of the same
* source. For example, two MediaExcerpts are equivalent if they represent the same speech in the
* same part of the same video, even if they are different resolutions or cropped differently.
* However, two MediaExcerpts are not equivalent if they represent the same speech in different
* parts of the same video.
*
*/
export const MediaExcerpt = Entity.extend({
/**
* One or more local representations of the excerpt.
Expand All @@ -486,6 +504,8 @@ export const MediaExcerpt = Entity.extend({
* Text-based:
* - focusText: a part of quotation that is the substance of the excerpt, while the rest of
* quotation provides additional context. The focusText must appear within the quotation.
* - contextText: text that encompasses the focusText to provie additional context, but which
* does not convey the substance of the excerpt.
* - description: a textual description of non-textual content. Like an img alt text. (How do
* users provite signal for an inaccurate description? The more literal the localRep, the less
* possibility for interpretation.)
Expand All @@ -504,7 +524,11 @@ export const MediaExcerpt = Entity.extend({
*/
localRep: z.object({
/**
* Text or speech that literally appears in the media.
* Text or speech that literally appears in the media and conveys the substance of the speech.
*
* Users may use this field either for focusText or for contextText (as described above.) It's
* an open question how we would migrate this field to a focusText/contextText split if we
* decided to do that.
*
* For textual media, this text must appear in the media. For audio and video media, this
* text must be a transcription of the speech that appears in the media.
Expand Down Expand Up @@ -659,6 +683,10 @@ export type Justification = Entity & {
type: "PROPOSITION_COMPOUND";
entity: PropositionCompound;
}
| {
type: "MEDIA_EXCERPT";
entity: MediaExcerpt;
}
/* @deprecated TODO(38) Replace with MediaExcerpt */
| {
type: "SOURCE_EXCERPT";
Expand Down Expand Up @@ -691,6 +719,7 @@ export type CounterJustification = Justification & {
};
export const JustificationBasisType = z.enum([
"PROPOSITION_COMPOUND",
"MEDIA_EXCERPT",
"SOURCE_EXCERPT",
// deprecated
"WRIT_QUOTE",
Expand All @@ -711,6 +740,10 @@ const justificationBaseShape = {
type: z.literal(JustificationBasisTypes.PROPOSITION_COMPOUND),
entity: PropositionCompound,
}),
z.object({
type: z.literal("MEDIA_EXCERPT"),
entity: MediaExcerpt,
}),
z.object({
type: z.literal(JustificationBasisTypes.SOURCE_EXCERPT),
entity: SourceExcerpt,
Expand Down Expand Up @@ -979,6 +1012,7 @@ export type CreateJustificationInput = Entity & {
basis: {
type: JustificationBasisType;
propositionCompound: EntityOrRef<CreatePropositionCompoundInput>;
mediaExcerpt?: MediaExcerptRef;
sourceExcerpt: EntityOrRef<CreateSourceExcerptInput>;
// deprecated
writQuote: EntityOrRef<CreateWritQuoteInput>;
Expand All @@ -994,14 +1028,17 @@ export type CreateJustificationInput = Entity & {
const createJustificationBaseShape = {
...omit(justificationBaseShape, ["created"]),
basis: z.object({
type: z.enum(["PROPOSITION_COMPOUND", "SOURCE_EXCERPT", "WRIT_QUOTE"] as [
JustificationBasisType,
...JustificationBasisType[]
]),
type: z.enum([
"PROPOSITION_COMPOUND",
"MEDIA_EXCERPT",
"SOURCE_EXCERPT",
"WRIT_QUOTE",
] as [JustificationBasisType, ...JustificationBasisType[]]),
propositionCompound: z.union([
CreatePropositionCompound,
PropositionCompoundRef,
]),
mediaExcerpt: MediaExcerptRef.optional(),
sourceExcerpt: z.union([CreateSourceExcerpt, SourceExcerptRef]),
writQuote: z.union([CreateWritQuote, WritQuoteRef]),
justificationBasisCompound: Entity.optional(),
Expand All @@ -1010,14 +1047,17 @@ const createJustificationBaseShape = {
const createJustificationInputBaseShape = {
...omit(createJustificationBaseShape, ["basis"]),
basis: z.object({
type: z.enum(["PROPOSITION_COMPOUND", "SOURCE_EXCERPT", "WRIT_QUOTE"] as [
JustificationBasisType,
...JustificationBasisType[]
]),
type: z.enum([
"PROPOSITION_COMPOUND",
"MEDIA_EXCERPT",
"SOURCE_EXCERPT",
"WRIT_QUOTE",
] as [JustificationBasisType, ...JustificationBasisType[]]),
propositionCompound: z.union([
CreatePropositionCompoundInput,
PropositionCompoundRef,
]),
mediaExcerpt: MediaExcerptRef.optional(),
sourceExcerpt: z.union([CreateSourceExcerptInput, SourceExcerptRef]),
writQuote: z.union([CreateWritQuoteInput, WritQuoteRef]),
justificationBasisCompound: Entity.optional(),
Expand Down Expand Up @@ -1073,6 +1113,10 @@ export type CreateJustification = Simplify<
type: "PROPOSITION_COMPOUND";
entity: EntityOrRef<CreatePropositionCompound>;
}
| {
type: "MEDIA_EXCERPT";
entity: MediaExcerptRef;
}
| {
type: "SOURCE_EXCERPT";
entity: EntityOrRef<CreateSourceExcerpt>;
Expand Down Expand Up @@ -1104,6 +1148,10 @@ export const CreateJustification: z.ZodType<CreateJustification> = z.lazy(() =>
type: z.literal("PROPOSITION_COMPOUND"),
entity: z.union([CreatePropositionCompound, PropositionCompoundRef]),
}),
z.object({
type: z.literal("MEDIA_EXCERPT"),
entity: MediaExcerptRef,
}),
z.object({
type: z.literal("SOURCE_EXCERPT"),
entity: z.union([CreateSourceExcerpt, SourceExcerptRef]),
Expand Down
Loading

0 comments on commit 76d08d7

Please sign in to comment.