From a Casting Call to Search Conditions

How Castar reads a free-text casting call with a single LLM call, turns it into strict filters and soft preferences, and why it would rather show no one than the wrong actor.

A casting call is written for people. A typical one reads something like: Supporting male role, early 40s to early 50s. Fluent English required. 178 cm or taller. Swimming a plus. A warm but weathered face, short hair. Most calls on Castar are written in Korean; the examples in this post are English translations.

An actor profile is written for a database. On Castar it holds a small set of structured fields: gender, birth year, height, weight and skills picked from a fixed list. It also has photos.

Search sits between the two. This post describes how Castar turns the first into something it can run against the second, and the decisions we made along the way. The most important one is about what the system should do when the honest answer is “nobody.”

Every casting call, role and output in this post is made up for illustration.

Two kinds of conditions

Read a casting call closely and its conditions fall into two groups.

Symbolic conditions map onto a profile field. “Male” is a value of gender. “Early 40s to early 50s” is a range over age, which we can work out from birth year. “178 cm or taller” is a bound on height. “Fluent English” and “swimming” are entries in the skills list. For each of these, a profile either meets the condition or it doesn’t.

Semantic conditions have no field. “A warm but weathered face,” “sharp features” and “looks like he has worked outdoors all his life” describe appearance and impression, and the only place that information lives is the photos. How well an actor fits is a matter of degree, and partly of taste.

We handle the two separately on purpose, because they fail differently. A wrong symbolic match is plainly wrong: a 21-year-old for a role written as “50s.” A semantic match can always be argued about. If both are folded into one similarity score, one can make up for the other, and a strong appearance match can lift an actor who doesn’t meet the age requirement above one who does. Keeping them apart lets the symbolic side be exact and the semantic side be graded.

This post covers the symbolic side. Appearance goes through a separate image–text channel, which will get its own post.

One call, one small JSON

Pipeline for a fictional casting call: “Supporting male role, early 40s to early 50s. Fluent English required. 178 cm or taller. Swimming a plus. A warm but weathered face, short hair.” One LLM call outputs JSON. The symbolic branch gives structured fields — gender = male, age 40–53, height ≥ 178, English required, swimming preferred — applied as a strict filter. The semantic branch gives the appearance text “a man with a warm, weathered face, short hair,” matched against photos in a shared image–text space and used to rank. Both lead to a shortlist.
Figure 1. One LLM call splits a casting call into symbolic conditions, which become a strict filter over structured profile fields, and a short appearance description, which is matched against photos and used for ranking. Open full size.

Each casting call goes to a language model once. Along with the text, the prompt carries the rules described below and the platform’s skill vocabulary, and asks for one compact JSON object. With field names simplified for this post, the object has these parts:

Field What goes in it
gender "male", "female", or null if the call doesn’t say
age a numeric range {min, max}, or null
height_cm numeric bounds, only when the call gives a number
skills.required labels from the platform’s skill vocabulary
skills.preferred labels from the same vocabulary
appearance a short description of look and impression, for the image channel

Anything the call doesn’t state stays null or empty. The model is told to fill a field only when the text supports it. A missing filter excludes nobody, while a guessed one quietly excludes people.

We use a single call rather than a chain of them (one for age, one for skills, one for appearance, and so on). Latency and cost grow with the number of calls, and every extra call is another place for a timeout or a malformed response. The parts also depend on each other. Whether “swimming” is required or preferred depends on the sentence around it, and one pass sees the whole call at once. The cost is a longer prompt that has to be maintained as a single unit.

The rest of this section goes through the fields one at a time.

Gender

Gender is filled when the call states it, or when the role word itself carries it, as with “father” or “grandmother”. Otherwise it stays null. An occupation on its own, like “detective,” doesn’t count.

Age: decade phrases become numbers

Casting calls usually give age as a decade, split into thirds: early, mid and late. We map each third to a fixed span of final digits: early is X0–X3, mid is X4–X6 and late is X7–X9.

Phrase Range
40s 40–49
early 40s 40–43
mid 40s 44–46
late 40s 47–49
early 40s to early 50s 40–53

A span such as the last row takes the lower bound of its first phrase and the upper bound of its second.

Mapping “early 40s to early 50s” to ages 40 to 53 Two rows of ten ages each, 40 to 49 and 50 to 59. Each decade is divided into early (0 to 3), mid (4 to 6) and late (7 to 9). Ages 40 through 53 are highlighted: all of the forties and the early fifties. 40s 50s 4041424344 4546474849 50515253 545556575859 earlymidlate earlymidlate
Figure 2. “Early 40s to early 50s” becomes {"min": 40, "max": 53}: from the start of the early forties to the end of the early fifties. Highlighted ages are inside the range.

Not every call uses a decade phrase. Bare words such as “young man” or “middle-aged” are mapped to default ranges written into the prompt. The model doesn’t make up a range for each query. The defaults are one explicit, editable guess.

The output is always a pair of numbers, never a label. An earlier prototype of our search used two coarse age groups, “young” and “old,” and the line between them was never written down anywhere. A numeric range can be checked directly against a birth year, and when a range is wrong, you can see exactly how it’s wrong.

Height: only when there is a number

“178 cm or taller” becomes {"min": 178}. “Tall” becomes nothing. Turning a relative word into a cutoff would mean picking a number the producer never gave, and that number would silently remove everyone just below it.

Skills: only from our own vocabulary

Skills on a Castar profile come from a fixed vocabulary: languages, sports, instruments and so on. The model gets that list and may only answer with labels from it. Along the way it normalizes synonyms. “Can ride horses” and “horseback riding” both become the platform skill Horse riding. “Spanish-speaking” becomes Spanish, and “fluent English” becomes English.

The restriction matters because a filter is only useful if a profile can satisfy it. If the model wrote free-form skills, “horseback riding” would never match a profile that lists Horse riding, and a correct-looking filter would return nobody.

Free-form skill phrases mapped to platform skills On the left, phrases as written in casting calls: “can ride horses”, “horseback riding”, “Spanish-speaking” and “fluent English”. Arrows map both horse phrases to the platform skill Horse riding, “Spanish-speaking” to Spanish and “fluent English” to English. IN THE CASTING CALL PLATFORM SKILL “can ride horses” “horseback riding” “Spanish-speaking” “fluent English” Horse riding Spanish English
Figure 3. Different wordings of the same skill collapse onto one label from the platform’s vocabulary, so the filter asks for something a profile can actually contain.

Required or preferred

Casting calls separate what a role needs from what would be nice to have, and the separation is usually marked in the wording. Phrases like “a plus,” “preferred,” “nice to have” or “bonus points for” mark a condition as optional. “Swimming a plus” puts swimming under preferred. Skills stated as plain requirements, like “Fluent English required,” go under required.

Appearance text

Whatever describes look and impression goes into a short appearance string, which is passed to the image–text channel. Nothing in it becomes a filter.

Strict means strict

Once the conditions are extracted, the required ones are applied as a filter: gender, the age range (compared with the age we compute from birth year), height bounds and required skills. An actor who fails any of them is out.

If nobody passes, the search stops and tells the producer there are no matching actors yet. It doesn’t loosen the age range or drop the English requirement and try again.

We did it the other way at first. When the filtered pool came back empty, the earlier prototype dropped the weakest condition and retried until something came back. That looks helpful, but it changes the question without telling anyone. A producer who wrote “50s” and sees a 21-year-old at the top of the list will either waste time on a candidate who was never going to fit, or will stop trusting the rest of the list. It’s worse because the result looks like a match. Nothing on the screen says “we ignored your age requirement.”

Relax-and-retry versus strict filtering Two panels for a fictional call asking for a man in his 50s who speaks English. Left, relax and retry: the age condition is silently dropped and the list comes back with actors aged 21, 26 and 34. Right, strict: all three conditions stay, and the result is “No matching actors yet”, leaving the producer to decide what to loosen. RELAX AND RETRY STRICT male 50s English male 50s English dropped, silently Actor · age 21 Actor · age 26 Actor · age 34 #1#2#3 No matching actors yet The producer decides what to loosen.
Figure 4. The same fictional call, “a man in his 50s, English required,” against a fictional pool with no such actor. Relaxing quietly answers a different question; the strict filter answers the one that was asked.

An empty result carries information. It tells the producer that the platform doesn’t have this person yet, and the producer can decide which condition to loosen, since they know which ones actually matter for the role. It also tells us where the actor pool is thin.

Preferred conditions never remove anyone. They only affect the order of the actors who passed the strict filter: someone who meets a preferred condition moves up, and someone who doesn’t stays in the list.

We also don’t show numbers next to results: no “92% match” and no impression score. A similarity value from a model isn’t the probability that someone is right for a role, but a number on screen invites people to read it that way and to treat 87 versus 84 as a real difference. Results are shown in order, without scores.

A worked example

Here is the fictional casting call from Figure 1 in full.

Role
Supporting male role, early 40s to early 50s.
Required
Fluent English required.
Height
178 cm or taller.
Preferred
Swimming a plus.
Look
A warm but weathered face, short hair.

The single LLM call returns (field names and labels simplified):

{
  "filters": {
    "gender": "male",
    "age": { "min": 40, "max": 53 },
    "height_cm": { "min": 178, "max": null }
  },
  "skills": {
    "required": ["English"],
    "preferred": ["Swimming"]
  },
  "appearance": "a man with a warm, weathered face, short hair"
}

Line by line:

  • “Male” becomes "gender": "male". “Supporting” describes the size of the part, not the actor, so it doesn’t become a condition.
  • “Early 40s to early 50s” becomes 40–53, using the rule from Figure 2.
  • “178 cm or taller” has a number, so it becomes a lower bound on height. There is no upper bound.
  • “Fluent English required” is normalized to the platform skill English and, because the call says “required,” it’s a required skill.
  • “Swimming a plus” is marked as a plus, so Swimming is preferred.
  • “A warm but weathered face, short hair” becomes the appearance text. None of it becomes a filter.

The search then keeps only male actors aged 40 to 53, at least 178 cm tall, with English in their skills. If that set is empty, the producer sees “no matching actors yet.” If it isn’t, actors who swim move up, and the appearance channel orders the rest.

What this doesn’t solve yet

  • Vague age words depend on defaults. “Young man” becomes whatever range the prompt says it is. Those defaults are explicit and easy to change, but they’re still our guess about what a producer means, and producers won’t all agree with it.
  • The vocabulary has to be maintained. Every new skill and every new way of saying an old one (“horse riding,” “horseback riding,” “can ride a horse”) needs a home in the vocabulary. A skill that isn’t in the vocabulary can’t become a filter at all.
  • Birth year is coarse. Age computed from a birth year can be off by one near a birthday, so an actor at the edge of a range can land on either side of it.
  • Strictness cuts both ways. Because required conditions exclude people, an extraction mistake (a misread age, or a preferred skill marked as required) also excludes people without saying so. Strict filtering makes the quality of this one LLM call matter more, not less.
  • Evaluation is ongoing. We’re still measuring how reliably the extraction handles real-world phrasing, and we’ll write about it when we have results we trust.

Next

This post covered the symbolic half of a casting call: conditions that map onto fields and can be checked exactly. The other half, “a warm but weathered face,” “sharp features” and the rest of the appearance vocabulary, can’t be checked that way. It goes through a separate image–text channel, and that will be the subject of our next post.